001/* 
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019package org.apache.wiki.tags;
020
021import java.io.IOException;
022import java.io.UnsupportedEncodingException;
023import java.net.URLDecoder;
024import java.net.URLEncoder;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.Map;
028
029import javax.servlet.http.Cookie;
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032import javax.servlet.jsp.PageContext;
033import javax.servlet.jsp.tagext.TagSupport;
034
035import org.apache.log4j.Logger;
036
037
038/**
039 * Sets or gets Cookie values. This implementation makes the following
040 * assumptions:
041 * <ul>
042 * <li>The cookie contains any number of name-value pairs
043 * <li>Name-value pairs are separated by "&" in the encoded cookie value string
044 * <li>An encoded name-value pair is compatible with JavaScript's
045 * encodeURIComponent(). Notably, spaces are encoded as "%20".
046 * <li>A decoded name-value pair separates the name and value with a "="
047 * </ul>
048 *
049 * <p>The value of a cookie carrying values n1="v1" and n2="v2 with space"
050 * would thus be
051 * <pre>
052 *   n1%3Dv1&n2%3Dv2%20with%20space
053 * </pre>
054 *
055 * <p>Usage:
056 *
057 * <pre>
058 * &lt;wiki:cookie name="cookiename" var="contextvariable" scope="page" /&gt;
059 * </pre>
060 * - Returns the value of the named cookie, or an empty string if not set.
061 * If 'var' is specified, the value is set into a context variable of this name.
062 * The 'scope' parameter may be added to specify the context: "session",
063 * "page", "request". If var is omitted, the output is placed directly into
064 * the JSP page.
065 *
066 * <pre>
067 * &lt;wiki:cookie name="cookiename" value="encoded_value" /&gt;
068 * </pre>
069 * - Sets the named cookie to the given value. If the value string is empty,
070 * the cookie value is set to empty; otherwise the cookie encoding rules of
071 * this class must be followed for the value.
072 *
073 * <pre>
074 * &lt;wiki:cookie name="cookiename" item="parameter_name" /&gt;
075 * </pre>
076 * - Assumes that the cookie contains URLEncoded name-value pairs,
077 * with name and value separated by an equals sign, and returns the value
078 * of the specified item.
079 *
080 * &lt;wiki:cookie name="cookiename" item="parameter_name" value="value" /&gt;
081 * </pre>
082 * - Sets the value of 'parameter_name' in the named cookie to 'value'.
083 *
084 * <pre>
085 * &lt;wiki:cookie name="cookiename" clear="parameter_name" /&gt;
086 * </pre>
087 * - Removes the named parameter from the cookie.
088 *
089 * <pre>
090 * &lt;wiki:cookie clear="cookiename" /&gt;
091 * </pre>
092 * - Removes the named cookie. Clear may be used at the same time as a value
093 * is retrieved (or set, despite the dubious usefulness of that operation).
094 */
095public class CookieTag
096    extends TagSupport
097{
098    private static final long serialVersionUID = 0L;
099
100    private static Logger log = Logger.getLogger( CookieTag.class );
101
102    /** Name of the cookie value. Required. */
103    private String m_name;
104    /** Name of the cookie nvp item. Optional. */
105    private String m_item;
106    /** A value to echo or set. Optional. */
107    private String m_value;
108    /** Name of a context variable to set result in. Optional, defaults to out.*/
109    private String m_var;
110    /** Scope of m_var: request, session, page. */
111    private String m_scope;
112    /** Name of a cookie or a cookie nvp to clear. */
113    private String m_clear;
114
115    /**
116     *  Set the "name" parameter.
117     *  
118     *  @param s The name.
119     */
120    public void setName( String s )
121    {
122        m_name = s;
123    }
124
125    /**
126     *  Set the "item" parameter.
127     *  
128     *  @param s The item.
129     */
130    public void setItem( String s )
131    {
132        m_item = s;
133    }
134
135    /**
136     *  Set the "value" parameter.
137     *  
138     *  @param s The value.
139     */
140    public void setValue( String s )
141    {
142        m_value = s;
143    }
144
145    /**
146     *  Set the "var" parameter.
147     *  
148     *  @param s The parameter.
149     */
150    public void setVar( String s )
151    {
152        m_scope = s;
153    }
154
155    /**
156     *  Set the "clear" parameter.
157     *  
158     *  @param s The parameter.
159     */
160    public void setClear( String s )
161    {
162        m_clear = s;
163    }
164
165    /**
166     *  Set the "scope" parameter.
167     *  
168     *  @param s The scope.
169     */
170    public void setScope( String s )
171    {
172        m_scope = s;
173    }
174
175    /**
176     *  {@inheritDoc}
177     */
178    public void release()
179    {
180        m_name = m_item = m_var = m_value = m_clear = m_scope = null;
181        super.release();
182    }
183
184    /**
185     * Examines the parameter and returns the corresponding scope identifier:
186     * "request" maps to PageContext.REQUEST_SCOPE, and so on.
187     * Possible values are "page", "session", "application", and "request",
188     * which is the default return value.
189     */
190    private int getScope( String s )
191    {
192        if( s == null )
193        {
194            return PageContext.REQUEST_SCOPE;
195        }
196        if( "page".equals( m_scope ) )
197        {
198            return PageContext.PAGE_SCOPE;
199        }
200        if( "session".equals( m_scope ) )
201        {
202            return PageContext.SESSION_SCOPE;
203        }
204        if( "application".equals( m_scope ) )
205        {
206            return PageContext.APPLICATION_SCOPE;
207        }
208
209        return PageContext.REQUEST_SCOPE;
210    }
211
212    /**
213     *  {@inheritDoc}
214     */
215    public int doEndTag()
216    {
217        String out = null;
218        Cookie cookie = findCookie( m_name );
219        boolean changed = false;
220
221        if( m_value != null )
222        {
223            if( m_item != null )
224            {
225                setItemValue( cookie, m_item, m_value );
226            }
227            else
228            {
229                cookie.setValue( m_value );
230            }
231            changed = true;
232        }
233        else
234        {
235            if( m_item != null )
236            {
237                out = getItemValue( cookie, m_item );
238            }
239            else
240            {
241                out = cookie.getValue();
242            }
243        }
244
245        if( out != null )
246        {
247            if( m_var != null )
248            {
249                int scope = getScope( m_scope );
250                pageContext.setAttribute( m_var, out,  scope );
251            }
252            else
253            {
254                try
255                {
256                    pageContext.getOut().print( out );
257                }
258                catch( IOException ioe )
259                {
260                    log.warn( "Failed to write to JSP page: " + ioe.getMessage(), ioe );
261                }
262            }
263        }
264
265        Cookie cleared = null;
266        if( m_clear != null )
267        {
268            cleared = findCookie( m_clear );
269            if( m_item != null )
270            {
271                setItemValue( cookie, m_item, null );
272            }
273            else
274            {
275                cleared.setValue( null );
276            }
277        }
278
279        HttpServletResponse res = (HttpServletResponse)pageContext.getResponse();
280        if( changed )
281        {
282            res.addCookie( cookie );
283        }
284        if( cleared != null )
285        {
286            res.addCookie( cleared );
287        }
288
289        return EVAL_PAGE;
290    }
291
292    /**
293     * Sets a single name-value pair in the given cookie.
294     */
295    private void setItemValue( Cookie c, String item, String value )
296    {
297        if( c == null )
298        {
299            return;
300        }
301        String in = c.getValue();
302        Map<String, String> values = parseCookieValues( in );
303        values.put( item, value );
304        String cv = encodeValues( values );
305        c.setValue( cv );
306    }
307
308    /**
309     * Returns the value of the given item in the cookie.
310     */
311    private String getItemValue( Cookie c, String item )
312    {
313        if( c == null || item == null ) {
314            return null;
315        }
316        String in = c.getValue();
317        Map< String, String > values = parseCookieValues( in );
318        return values.get( item );
319    }
320
321
322    /**
323     * Parses a cookie value, of format name1%3Fvalue1&name2%3Fvalue2...,
324     * into a Map<String,String>.
325     */
326    private Map<String, String> parseCookieValues( String s )
327    {
328        Map< String, String > rval = new HashMap< String, String >();
329        if( s == null ) {
330            return rval;
331        }
332        String[] nvps = s.split( "&" );
333        if( nvps.length == 0 ) {
334            return rval;
335        }
336        for( int i = 0; i < nvps.length; i++ ) {
337            String nvp = decode( nvps[i] );
338            String[] nv = nvp.split( "=" );
339            if( nv[0] != null && nv[0].trim().length() > 0 )
340            {
341                rval.put( nv[0], nv[1] );
342            }
343        }
344
345        return rval;
346    }
347
348    /**
349     * Encodes name-value pairs in the map into a single string, in a format
350     * understood by this class and JavaScript decodeURIComponent().
351     */
352    private String encodeValues( Map<String, String> values )
353    {
354        StringBuilder rval = new StringBuilder();
355        if( values == null || values.size() == 0 ) {
356            return rval.toString();
357        }
358
359        Iterator< Map.Entry< String, String > > it = values.entrySet().iterator();
360        while( it.hasNext() ) {
361            Map.Entry< String, String > e = it.next();
362            String n = e.getKey();
363            String v = e.getValue();
364            if( v != null ) {
365                String nv = n + "=" + v;
366                rval.append( encode( nv ) );
367            }
368        }
369
370        return rval.toString();
371    }
372
373    /**
374     * Converts a String to an encoding understood by JavaScript
375     * decodeURIComponent.
376     */
377    private String encode( String nvp )
378    {
379        String coded = "";
380        try
381        {
382            coded = URLEncoder.encode( nvp, "UTF-8" );
383        }
384        catch( UnsupportedEncodingException e )
385        {
386            /* never happens */
387            log.info( "Failed to encode UTF-8", e );
388        }
389        return coded.replaceAll( "\\+", "%20" );
390    }
391
392    /**
393     * Converts a cookie value (set by this class, or by a JavaScript
394     * encodeURIComponent call) into a plain string.
395     */
396    private String decode( String envp )
397    {
398        String rval;
399        try
400        {
401            rval = URLDecoder.decode( envp , "UTF-8" );
402            return rval;
403        }
404        catch( UnsupportedEncodingException e )
405        {
406            log.error( "Failed to decode cookie", e );
407            return envp;
408        }
409    }
410
411    /**
412     * Locates the named cookie in the request, or creates a new one if it
413     * doesn't exist.
414     */
415    private Cookie findCookie( String cname )
416    {
417        HttpServletRequest req = (HttpServletRequest)pageContext.getRequest();
418        if( req != null )
419        {
420            Cookie[] cookies = req.getCookies();
421            if( cookies != null )
422            {
423                for( int i = 0; i < cookies.length; i++ )
424                {
425                    if( cookies[i].getName().equals( cname ) )
426                    {
427                        return cookies[i];
428                    }
429                }
430            }
431        }
432
433        return new Cookie( cname, null );
434    }
435
436}