View Javadoc

1   package org.apache.velocity.tools.generic;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.UnsupportedEncodingException;
23  import java.net.URI;
24  import java.net.URLDecoder;
25  import java.net.URLEncoder;
26  import java.util.ArrayList;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import org.apache.velocity.runtime.log.Log;
31  import org.apache.velocity.tools.Scope;
32  import org.apache.velocity.tools.ToolContext;
33  import org.apache.velocity.tools.config.DefaultKey;
34  import org.apache.velocity.tools.config.SkipSetters;
35  import org.apache.velocity.tools.config.ValidScope;
36  
37  /**
38   * <p>The LinkTool provides many methods to work with URIs and can help you:
39   * <ul>
40   *     <li>construct full URIs (opaque, absolute or relative)</li>
41   *     <li>encode and decode URLs (part or whole)</li>
42   *     <li>retrieve path info for the current request</li>
43   *     <li>and more..</li>
44   * </ul></p>
45   *
46   * <p>This GenericTools (i.e. non-servlet based) version of LinkTool
47   * is largely based upon the same API and behavior as the older
48   * VelocityView version, with a few differences, particularly in
49   * internal representation and query handling.  You can expect that
50   * in the future work will be done to more closely align the APIs.
51   * It is likely that the VelocityView version will become a subclass
52   * of this version that adds on servlet-awareness and related features.
53   * For now, though, they are entirely separate but similar tools.
54   * </p>
55   *
56   * <p>The LinkTool is somewhat special in that nearly all public methods return
57   * a new instance of LinkTool. This facilitates greatly the repeated use
58   * of the LinkTool in Velocity and leads to an elegant syntax.</p>
59   * 
60   * <p><pre>
61   * Template example(s):
62   *   #set( $base = $link.relative('MyPage.vm').anchor('view') )
63   *   &lt;a href="$base.param('select','this')"&gt;this&lt;/a&gt;
64   *   &lt;a href="$base.param('select','that')"&gt;that&lt;/a&gt;
65   *
66   * Toolbox configuration:
67   * &lt;tools&gt;
68   *   &lt;toolbox scope="request"&gt;
69   *     &lt;tool class="org.apache.velocity.tools.generic.LinkTool"
70   *              uri="http://velocity.apache.org/tools/devel/"/&gt;
71   *   &lt;/toolbox&gt;
72   * &lt;/tools&gt;
73   * </pre></p>
74   *
75   * @author Nathan Bubna
76   * @since VelocityTools 2.0
77   * @version $Id: LinkTool.java 601976 2007-12-07 03:50:51Z nbubna $
78   */
79  @DefaultKey("link")
80  @SkipSetters
81  @ValidScope(Scope.REQUEST)
82  public class LinkTool extends SafeConfig implements Cloneable
83  {
84      /** Standard HTML delimiter for query data ('&') */
85      public static final String HTML_QUERY_DELIMITER = "&";
86  
87      /** XHTML delimiter for query data ('&amp;amp;') */
88      public static final String XHTML_QUERY_DELIMITER = "&amp;";
89  
90      public static final String APPEND_PARAMS_KEY = "appendParameters";
91      public static final String FORCE_RELATIVE_KEY = "forceRelative";
92      public static final String DEFAULT_CHARSET = "UTF-8";
93      public static final String DEFAULT_SCHEME = "http";
94      public static final String SECURE_SCHEME = "https";
95  
96      public static final String URI_KEY = "uri";
97      public static final String SCHEME_KEY = "scheme";
98      public static final String USER_KEY = "user";
99      public static final String HOST_KEY = "host";
100     public static final String PORT_KEY = "port";
101     public static final String PATH_KEY = ToolContext.PATH_KEY;
102     public static final String QUERY_KEY = "params";
103     public static final String FRAGMENT_KEY = "anchor";
104     public static final String CHARSET_KEY = "charset";
105     public static final String XHTML_MODE_KEY = "xhtml";
106 
107     protected Log LOG;
108     protected String scheme;
109     protected String user;
110     protected String host;
111     protected int port;
112     protected String path;
113     protected Map query;
114     protected String fragment;
115     protected String charset;
116     protected String queryDelim;
117     protected boolean appendParams;
118     protected boolean forceRelative;
119     protected boolean opaque;
120     protected final LinkTool self;
121 
122 
123     /**
124      * Default constructor. Tool typically is configured before use.
125      */
126     public LinkTool()
127     {
128         scheme = null;
129         user = null;
130         host = null;
131         port = -1;
132         path = null;
133         query = null;
134         fragment = null;
135         charset = DEFAULT_CHARSET;
136         queryDelim = XHTML_QUERY_DELIMITER;
137         opaque = false;
138         appendParams = true;
139         forceRelative = false;
140         self = this;
141     }
142 
143     protected final void debug(String msg, Object... args)
144     {
145         debug(msg, null, args);
146     }
147 
148     protected final void debug(String msg, Throwable t, Object... args)
149     {
150         if (LOG != null && LOG.isDebugEnabled())
151         {
152             LOG.debug("LinkTool: "+String.format(msg, args), t);
153         }
154     }
155 
156 
157     // --------------------------------------- Setup Methods -------------
158 
159     protected void configure(ValueParser props)
160     {
161         this.LOG = (Log)props.getValue(ToolContext.LOG_KEY);
162 
163         String link = props.getString(URI_KEY);
164         if (link != null)
165         {
166             setFromURI(link);
167         }
168 
169         String schm = props.getString(SCHEME_KEY);
170         if (schm != null)
171         {
172             setScheme(schm);
173         }
174         String info = props.getString(USER_KEY);
175         if (info != null)
176         {
177             setUserInfo(info);
178         }
179         String hst = props.getString(HOST_KEY);
180         if (hst != null)
181         {
182             setHost(hst);
183         }
184         Integer prt = props.getInteger(PORT_KEY);
185         if (prt != null)
186         {
187             setPort(prt.intValue());
188         }
189         String pth = props.getString(PATH_KEY);
190         if (pth != null)
191         {
192             setPath(pth);
193         }
194         String params = props.getString(QUERY_KEY);
195         if (params != null)
196         {
197             setQuery(params);
198         }
199         String anchor = props.getString(FRAGMENT_KEY);
200         if (anchor != null)
201         {
202             setFragment(anchor);
203         }
204 
205         String chrst = props.getString(CHARSET_KEY);
206         if (chrst != null)
207         {
208             setCharacterEncoding(chrst);
209         }
210 
211         Boolean xhtml = props.getBoolean(XHTML_MODE_KEY);
212         if (xhtml != null)
213         {
214             setXHTML(xhtml);
215         }
216         Boolean addParams = props.getBoolean(APPEND_PARAMS_KEY);
217         if (addParams != null)
218         {
219             setAppendParams(addParams);
220         }
221         Boolean forceRelative = props.getBoolean(FORCE_RELATIVE_KEY);
222         if (forceRelative != null)
223         {
224             setForceRelative(forceRelative);
225         }
226     }
227 
228     /**
229      * Equivalent to clone, but with no checked exceptions.
230      * If for some unfathomable reason clone() doesn't work,
231      * this will throw a RuntimeException.
232      */
233     protected LinkTool duplicate()
234     {
235         return duplicate(false);
236     }
237 
238     /**
239      * Equivalent to clone, but with no checked exceptions.
240      * If for some unfathomable reason clone() doesn't work,
241      * this will throw a RuntimeException.  If doing a deep
242      * clone, then the parameter Map will also be cloned.
243      */
244     protected LinkTool duplicate(boolean deep)
245     {
246         try
247         {
248             LinkTool that = (LinkTool)this.clone();
249             if (deep && query != null)
250             {
251                 that.query = new LinkedHashMap(query);
252             }
253             return that;
254         }
255         catch (CloneNotSupportedException e)
256         {
257             String msg = "Could not properly clone " + getClass();
258             if (LOG != null)
259             {
260                 LOG.error(msg, e);
261             }
262             throw new RuntimeException(msg, e);
263         }
264     }
265 
266     public void setCharacterEncoding(String chrst)
267     {
268         this.charset = chrst;
269     }
270 
271     /**
272      * <p>Controls the delimiter used for separating query data pairs.
273      *    By default, the standard '&' character is used.</p>
274      * <p>This is not exposed to templates as this decision is best not
275      *    made at that level.</p>
276      * <p>Subclasses may easily override the init() method to set this
277      *    appropriately and then call super.init()</p>
278      *
279      * @param xhtml if true, the XHTML query data delimiter ('&amp;amp;')
280      *        will be used.  if false, then '&' will be used.
281      * @see <a href="http://www.w3.org/TR/xhtml1/#C_12">Using Ampersands in Attribute Values (and Elsewhere)</a>
282      */
283     public void setXHTML(boolean xhtml)
284     {
285         queryDelim = (xhtml) ? XHTML_QUERY_DELIMITER : HTML_QUERY_DELIMITER;
286     }
287 
288     /**
289      * Sets whether or not the {@link #setParam} method
290      * will override existing query values for the same key or simply append
291      * the new value to a list of existing values.
292      */
293     public void setAppendParams(boolean addParams)
294     {
295         this.appendParams = addParams;
296     }
297 
298     /**
299      * Sets whether or not the {@link #createURI} method should ignore the
300      * scheme, user, port and host values for non-opaque URIs, thus making
301      * {@link #toString} print the link as a relative one, not an absolute
302      * one.  NOTE: using {@link #absolute()}, {@link #absolute(Object)},
303      * {@link #relative()}, or {@link #relative(Object)} will alter this
304      * setting accordingly on the new instances they return.
305      */
306     public void setForceRelative(boolean forceRelative)
307     {
308         this.forceRelative = forceRelative;
309     }
310 
311     /**
312      * This will treat empty strings like null values
313      * and will trim any trailing ':' character.
314      */
315     public void setScheme(Object obj)
316     {
317         if (obj == null)
318         {
319             this.scheme = null;
320         }
321         else
322         {
323             this.scheme = String.valueOf(obj);
324             if (scheme.length() == 0)
325             {
326                 this.scheme = null;
327             }
328             if (scheme.endsWith(":"))
329             {
330                 this.scheme = scheme.substring(0, scheme.length() - 1);
331             }
332         }
333     }
334 
335     public void setUserInfo(Object obj)
336     {
337         this.user = obj == null ? null : String.valueOf(obj);
338     }
339 
340     public void setHost(Object obj)
341     {
342         this.host = obj == null ? null : String.valueOf(obj);
343     }
344 
345     /**
346      * If the specified object is null, this will set the port value
347      * to -1 to indicate that.  If it is non-null and cannot be converted
348      * to an integer, then it will be set to -2 to indicate an error.
349      */
350     public void setPort(Object obj)
351     {
352         if (obj == null)
353         {
354             this.port = -1;
355         }
356         else if (obj instanceof Number)
357         {
358             this.port = ((Number)obj).intValue();
359         }
360         else
361         {
362             try
363             {
364                 this.port = Integer.parseInt(String.valueOf(obj));
365             }
366             catch (NumberFormatException nfe)
367             {
368                 debug("Could convert '%s' to int", nfe, obj);
369                 this.port = -2; // use this to mean error
370             }
371         }
372     }
373 
374     /**
375      * If this instance is not opaque and the specified value does
376      * not start with a '/' character, then that will be prepended
377      * automatically.
378      */
379     public void setPath(Object obj)
380     {
381         if (obj == null)
382         {
383             this.path = null;
384         }
385         else
386         {
387             this.path = String.valueOf(obj);
388             if (!this.opaque && !path.startsWith("/"))
389             {
390                 this.path = '/' + this.path;
391             }
392         }
393     }
394 
395     /**
396      * Uses {@link #combinePath} to add the specified value
397      * to the current {@link #getPath} value.  If the specified
398      * value is null or this instance is opaque, then this is
399      * a no-op.
400      */
401     public void appendPath(Object obj)
402     {
403         if (obj != null && !this.opaque)
404         {
405             setPath(combinePath(getPath(), String.valueOf(obj)));
406         }
407     }
408 
409     /**
410      * If end is null, this will return start and vice versa.
411      * If neither is null, this will append the end to the start,
412      * making sure that there is only one '/' character between
413      * the two values.
414      */
415     protected String combinePath(String start, String end)
416     {
417         if (end == null)
418         {
419             return start;
420         }
421         if (start == null)
422         {
423             return end;
424         }
425 
426         // make sure we don't get // or nothing between start and end
427         boolean startEnds = start.endsWith("/");
428         boolean endStarts = end.startsWith("/");
429         if (startEnds ^ endStarts) //one
430         {
431             return start + end;
432         }
433         else if (startEnds & endStarts) //both
434         {
435             return start + end.substring(1, end.length());
436         }
437         else //neither
438         {
439             return start + '/' + end;
440         }
441     }
442 
443     /**
444      * If the specified value is null, it will set the query to null.
445      * If a Map, it will copy all those values into a new LinkedHashMap and
446      * replace any current query value with that. If it is a String,
447      * it will use {@link #parseQuery(String)} to parse it into a map
448      * of keys to values.
449      */
450     public void setQuery(Object obj)
451     {
452         if (obj == null)
453         {
454             this.query = null;
455         }
456         else if (obj instanceof Map)
457         {
458             this.query = new LinkedHashMap((Map)obj);
459         }
460         else
461         {
462             String qs = normalizeQuery(String.valueOf(obj));
463             this.query = parseQuery(qs);
464         }
465     }
466 
467     protected String normalizeQuery(String qs)
468     {
469         // if we have multiple pairs...
470         if (qs.indexOf('&') >= 0)
471         {
472             // ensure the delimeters match the xhtml setting
473             // this impl is not at all efficient, but it's easy
474             qs = qs.replaceAll("&(amp;)?", queryDelim);
475         }
476         return qs;
477     }
478 
479     /**
480      * Converts the map of keys to values into a query string.
481      */
482     public String toQuery(Map parameters)
483     {
484         if (parameters == null)
485         {
486             return null;
487         }
488         StringBuilder query = new StringBuilder();
489         for (Object e : parameters.entrySet())
490         {
491             Map.Entry entry = (Map.Entry)e;
492             //add new pair to this LinkTool's query data
493             if (query.length() > 0)
494             {
495                 query.append(queryDelim);
496             }
497             query.append(toQuery(entry.getKey(), entry.getValue()));
498         }
499         return query.toString();
500     }
501 
502     /**
503      * Uses {@link #combineQuery} to append the specified value
504      * to the current {@link #getQuery} value.
505      */
506     public void appendQuery(Object obj)
507     {
508         if (obj != null)
509         {
510             setQuery(combineQuery(getQuery(), String.valueOf(obj)));
511         }
512     }
513 
514     /**
515      * If there is no existing value for this key in the query, it
516      * will simply add it and its value to the query.  If the key
517      * already is present in the query and append
518      * is true, this will add the specified value to those
519      * already under that key.  If {@link #appendParams} is
520      * false, this will override the existing values with the
521      * specified new value.
522      */
523     public void setParam(Object key, Object value, boolean append)
524     {
525         // use all keys as strings, even null -> "null"
526         key = String.valueOf(key);
527         if (this.query == null)
528         {
529             this.query = new LinkedHashMap();
530             putParam(key, value);
531         }
532         else if (append)
533         {
534             appendParam((String)key, value);
535         }
536         else
537         {
538             putParam(key, value);
539         }
540     }
541 
542     private void appendParam(String key, Object value)
543     {
544         if (query.containsKey(key))
545         {
546             Object cur = query.get(key);
547             if (cur instanceof List)
548             {
549                 addToList((List)cur, value);
550             }
551             else
552             {
553                 List vals = new ArrayList();
554                 vals.add(cur);
555                 addToList(vals, value);
556                 putParam(key, vals);
557             }
558         }
559         else
560         {
561             putParam(key, value);
562         }
563     }
564 
565     private void putParam(Object key, Object value)
566     {
567         if (value instanceof Object[])
568         {
569             List vals = new ArrayList();
570             for (Object v : ((Object[])value))
571             {
572                 vals.add(v);
573             }
574             value = vals;
575         }
576         query.put(key, value);
577     }
578 
579     private void addToList(List vals, Object value)
580     {
581         if (value instanceof List)
582         {
583             for (Object v : ((List)value))
584             {
585                 vals.add(v);
586             }
587         }
588         else if (value instanceof Object[])
589         {
590             for (Object v : ((Object[])value))
591             {
592                 vals.add(v);
593             }
594         }
595         else
596         {
597             vals.add(value);
598         }
599     }
600 
601     /**
602      * If append is false, this simply delegates to {@link #setQuery}.
603      * Otherwise, if the specified object is null, it does nothing.  If the object
604      * is not a Map, it will turn it into a String and use {@link #parseQuery} to
605      * parse it. Once it is a Map, it will iterate through the entries appending
606      * each key/value to the current query data.
607      */
608     public void setParams(Object obj, boolean append)
609     {
610         if (!append)
611         {
612             setQuery(obj);
613         }
614         else if (obj != null)
615         {
616             if (!(obj instanceof Map))
617             {
618                 obj = parseQuery(String.valueOf(obj));
619             }
620             if (obj != null)
621             {
622                 if (query == null)
623                 {
624                     this.query = new LinkedHashMap();
625                 }
626                 for (Object e : ((Map)obj).entrySet())
627                 {
628                     Map.Entry entry = (Map.Entry)e;
629                     String key = String.valueOf(entry.getKey());
630                     appendParam(key, entry.getValue());
631                 }
632             }
633         }
634     }
635 
636     /**
637      * Removes the query pair(s) with the specified key from the
638      * query data and returns the remove value(s), if any.
639      */
640     public Object removeParam(Object key)
641     {
642         if (query != null)
643         {
644             key = String.valueOf(key);
645             return query.remove(key);
646         }
647         return null;
648     }
649 
650     /**
651      * In this class, this method ignores true values.  If passed a false value,
652      * it will call {@link #setQuery} with a null value to clear all query data.
653      */
654     protected void handleParamsBoolean(boolean keep)
655     {
656         if (!keep)
657         {
658             setQuery(null);
659         }
660     }
661 
662     /**
663      * If the second param is null or empty, this will simply return the first
664      * and vice versa.  Otherwise, it will trim any '?'
665      * at the start of the second param and any '&amp;' or '&amp;amp;' at the
666      * end of the first one, then combine the two, making sure that they
667      * are separated by only one delimiter.
668      */
669     protected String combineQuery(String current, String add)
670     {
671         if (add == null || add.length() == 0)
672         {
673             return current;
674         }
675         if (add.startsWith("?"))
676         {
677             add = add.substring(1, add.length());
678         }
679         if (current == null || current.length() == 0)
680         {
681             return add;
682         }
683         if (current.endsWith(queryDelim))
684         {
685             current = current.substring(0, current.length() - queryDelim.length());
686         }
687         else if (current.endsWith("&"))
688         {
689             current = current.substring(0, current.length() - 1);
690         }
691         if (add.startsWith(queryDelim))
692         {
693             return current + add;
694         }
695         else if (add.startsWith("&"))
696         {
697             // drop the html delim in favor of the xhtml one
698             add = add.substring(1, add.length());
699         }
700         return current + queryDelim + add;
701     }
702 
703     /**
704      * Turns the specified key and value into a properly encoded
705      * query pair string.  If the value is an array or List, then
706      * this will create a delimited string of query pairs, reusing 
707      * the same key for each of the values separately.
708      */
709     protected String toQuery(Object key, Object value)
710     {
711         StringBuilder out = new StringBuilder();
712         if (value == null)
713         {
714             out.append(encode(key));
715             out.append('=');
716             /* Interpret null as "no value" */
717         }
718         else if (value instanceof List)
719         {
720             appendAsArray(out, key, ((List)value).toArray());
721         }
722         else if (value instanceof Object[])
723         {
724             appendAsArray(out, key, (Object[])value);
725         }
726         else
727         {
728             out.append(encode(key));
729             out.append('=');
730             out.append(encode(value));
731         }
732         return out.toString();
733     }
734 
735     /* Utility method to avoid logic duplication in toQuery() */
736     protected void appendAsArray(StringBuilder out, Object key, Object[] arr)
737     {
738         String encKey = encode(key);
739         for (int i=0; i < arr.length; i++)
740         {
741             out.append(encKey);
742             out.append('=');
743             if (arr[i] != null)
744             {
745                 out.append(encode(arr[i]));
746             }
747             if (i+1 < arr.length)
748             {
749                 out.append(queryDelim);
750             }
751         }
752     }
753 
754     /**
755      * Uses {@link #normalizeQuery} to make all delimiters in the
756      * specified query string match the current query delimiter 
757      * and then uses {@link #parseQuery(String,String)} to parse it
758      * according to that same delimiter.
759      */
760     protected Map<String,Object> parseQuery(String query)
761     {
762         return parseQuery(normalizeQuery(query), this.queryDelim);
763     }
764 
765     /**
766      * This will use the specified query delimiter to parse the specified
767      * query string into a map of keys to values.
768      * If there are multiple query pairs in the string that have the same
769      * key, then the values will be combined into a single List value
770      * associated with that key.
771      */
772     protected Map<String,Object> parseQuery(String query, String queryDelim)
773     {
774         if (query.startsWith("?"))
775         {
776             query = query.substring(1, query.length());
777         }
778         String[] pairs = query.split(queryDelim);
779         if (pairs.length == 0)
780         {
781             return null;
782         }
783         Map<String,Object> params = new LinkedHashMap<String,Object>(pairs.length);
784         for (String pair : pairs)
785         {
786             String[] kv = pair.split("=");
787             String key = kv[0];
788             Object value = kv.length > 1 ? kv[1] : null;
789             if (params.containsKey(kv[0]))
790             {
791                 Object oldval = params.get(key);
792                 if (oldval instanceof List)
793                 {
794                     ((List)oldval).add((String)value);
795                     value = oldval;
796                 }
797                 else
798                 {
799                     List<String> list = new ArrayList<String>();
800                     list.add((String)oldval);
801                     list.add((String)value);
802                     value = list;
803                 }
804             }
805             params.put(key, value);
806         }
807         return params;
808     }
809 
810     /**
811      * Sets the anchor for this instance and treats empty strings like null.
812      */
813     public void setFragment(Object obj)
814     {
815         if (obj == null)
816         {
817             this.fragment = null;
818         }
819         else
820         {
821             this.fragment = String.valueOf(obj);
822             if (this.fragment.length() == 0)
823             {
824                 this.fragment = null;
825             }
826         }
827     }
828 
829     /**
830      * If the specified value is null, this will set the scheme, userInfo,
831      * host, port, path, query, and fragment all to their null-equivalent
832      * values.  Otherwise, this will
833      * convert the specified object into a {@link URI}, then those same
834      * values from the URI object to this instance.
835      */
836     protected boolean setFromURI(Object obj)
837     {
838         if (obj == null)
839         {
840             // clear everything out...
841             setScheme(null);
842             setUserInfo(null);
843             setHost(null);
844             setPort(null);
845             setPath(null);
846             setQuery(null);
847             setFragment(null);
848             return true;
849         }
850 
851         URI uri = toURI(obj);
852         if (uri == null)
853         {
854             return false;
855         }
856         setScheme(uri.getScheme());
857         if (uri.isOpaque())
858         {
859             this.opaque = true;
860             // path is used as scheme-specific part
861             setPath(uri.getSchemeSpecificPart());
862         }
863         else
864         {
865             setUserInfo(uri.getUserInfo());
866             setHost(uri.getHost());
867             setPort(uri.getPort());
868             String pth = uri.getPath();
869             if (pth.equals("/") || pth.length() == 0)
870             {
871                 pth = null;
872             }
873             setPath(pth);
874             setQuery(uri.getQuery());
875         }
876         setFragment(uri.getFragment());
877         return true;
878     }
879 
880     /**
881      * Turns the specified object into a string and thereby a URI.
882      */
883     protected URI toURI(Object obj)
884     {
885         if (obj instanceof URI)
886         {
887             return (URI)obj;
888         }
889         else
890         {
891             try
892             {
893                 return new URI(String.valueOf(obj));
894             }
895             catch (Exception e)
896             {
897                 debug("Could convert '%s' to URI", e, obj);
898                 return null;
899             }
900         }
901     }
902 
903     /**
904      * Tries to create a URI from the current port, opacity, scheme,
905      * userInfo, host, path, query and fragment set for this instance,
906      * using the {@link URI} constructor that is appropriate to the opacity.
907      */
908     protected URI createURI()
909     {
910         try
911         {
912             // fail if there was an error in setting the port
913             if (port > -2)
914             {
915                 if (opaque)
916                 {
917                     // path is used as scheme-specific part
918                     return new URI(scheme, path, fragment);
919                 }
920                 else if (forceRelative)
921                 {
922                     if (path == null && query == null && fragment == null)
923                     {
924                         return null;
925                     }
926                     return new URI(null, null, null, -1, path, toQuery(query), fragment);
927                 }
928                 else
929                 {
930                     // only create the URI if we have some values besides a port
931                     if (scheme == null && user == null && host == null
932                         && path == null && query == null && fragment == null)
933                     {
934                         return null;
935                     }
936                     return new URI(scheme, user, host, port, path, toQuery(query), fragment);
937                 }
938             }
939         }
940         catch (Exception e)
941         {
942             debug("Could not create URI", e);
943         }
944         return null;
945     }
946 
947     // --------------------------------------------- Template Methods -----------
948 
949     /**
950      * Returns the configured charset used by the {@link #encode} and
951      * {@link #decode} methods.
952      */
953     public String getCharacterEncoding()
954     {
955         return this.charset;
956     }
957 
958     /**
959      * Returns true if the query delimiter used by this instance is
960      * using <code>&amp;amp;</code> as the delimiter for query data pairs
961      * or just using <code>&amp;</code>.
962      */
963     public boolean isXHTML()
964     {
965         return queryDelim.equals(XHTML_QUERY_DELIMITER);
966     }
967 
968     /**
969      * Returns true if {@link #param(Object,Object)} appends values;
970      * false if the method overwrites existing value(s) for the specified key.
971      */
972     public boolean getAppendParams()
973     {
974         return this.appendParams;
975     }
976 
977 
978     /**
979      * Returns a new instance with the specified value set as its scheme.
980      */
981     public LinkTool scheme(Object scheme)
982     {
983         LinkTool copy = duplicate();
984         copy.setScheme(scheme);
985         return copy;
986     }
987 
988     /**
989      * Returns a new instance with the scheme set to "https".
990      */
991     public LinkTool secure()
992     {
993         return scheme(SECURE_SCHEME);
994     }
995 
996     /**
997      * Returns a new instance with the scheme set to "http".
998      */
999     public LinkTool insecure()
1000     {
1001         return scheme(DEFAULT_SCHEME);
1002     }
1003 
1004     /**
1005      * Return the scheme value for this instance.
1006      */
1007     public String getScheme()
1008     {
1009         return scheme;
1010     }
1011 
1012     /**
1013      * Returns true if this instance's scheme is "https".
1014      */
1015     public boolean isSecure()
1016     {
1017         return SECURE_SCHEME.equalsIgnoreCase(getScheme());
1018     }
1019 
1020     /**
1021      * Returns true if this instance represents an opaque URI.
1022      * @see URI
1023      */
1024     public boolean isOpaque()
1025     {
1026         return this.opaque;
1027     }
1028 
1029     /**
1030      * Returns a new instance with the specified value
1031      * set as its user info.
1032      */
1033     public LinkTool user(Object info)
1034     {
1035         LinkTool copy = duplicate();
1036         copy.setUserInfo(info);
1037         return copy;
1038     }
1039 
1040     /**
1041      * Returns the {@link URI#getUserInfo()} value for this instance.
1042      */
1043     public String getUser()
1044     {
1045         return this.user;
1046     }
1047 
1048     /**
1049      * Returns a new instance with the specified value set as its
1050      * host.  If no scheme has yet been set, the new instance will
1051      * also have its scheme set to the {@link #DEFAULT_SCHEME} (http).
1052      */
1053     public LinkTool host(Object host)
1054     {
1055         LinkTool copy = duplicate();
1056         copy.setHost(host);
1057         // if we have host but no scheme
1058         if (copy.getHost() != null && !copy.isAbsolute())
1059         {
1060             // use default scheme
1061             copy.setScheme(DEFAULT_SCHEME);
1062         }
1063         return copy;
1064     }
1065 
1066     /**
1067      * Return the host value for this instance.
1068      */
1069     public String getHost()
1070     {
1071         return this.host;
1072     }
1073 
1074     /**
1075      * Returns a new instance with the specified value set
1076      * as its port number.  If the value cannot be parsed into
1077      * an integer, the returned instance will always return
1078      * null for {@link #toString} and other
1079      * {@link #createURI}-dependent methods to alert the user
1080      * to the error.
1081      */
1082     public LinkTool port(Object port)
1083     {
1084         LinkTool copy = duplicate();
1085         copy.setPort(port);
1086         return copy;
1087     }
1088 
1089     /**
1090      * Returns the  port value, if any.
1091      */
1092     public Integer getPort()
1093     {
1094         if (this.port < 0)
1095         {
1096             return null;
1097         }
1098         return this.port;
1099     }
1100 
1101     /**
1102      * Returns a new instance with the specified value
1103      * set as its path.
1104      */
1105     public LinkTool path(Object pth)
1106     {
1107         LinkTool copy = duplicate();
1108         copy.setPath(pth);
1109         return copy;
1110     }
1111 
1112     /**
1113      * Returns the current path value for this instance.
1114      */
1115     public String getPath()
1116     {
1117         return this.path;
1118     }
1119 
1120     /**
1121      * Appends the given value to the end of the current
1122      * path value.
1123      */
1124     public LinkTool append(Object pth)
1125     {
1126         LinkTool copy = duplicate();
1127         copy.appendPath(pth);
1128         return copy;
1129     }
1130 
1131     /**
1132      * Returns the directory stack
1133      * in the set {@link #getPath()} value, by just trimming
1134      * off all that follows the last "/".
1135      */
1136     public String getDirectory()
1137     {
1138         if (this.path == null || this.opaque)
1139         {
1140             return null;
1141         }
1142         int lastSlash = this.path.lastIndexOf('/');
1143         if (lastSlash < 0)
1144         {
1145             return "";
1146         }
1147         return this.path.substring(0, lastSlash + 1);
1148     }
1149 
1150     /**
1151      * Returns the last section of the path,
1152      * which is all that follows the final "/".
1153      */
1154     public String getFile()
1155     {
1156         if (this.path == null || this.opaque)
1157         {
1158             return null;
1159         }
1160         int lastSlash = this.path.lastIndexOf('/');
1161         if (lastSlash < 0)
1162         {
1163             return this.path;
1164         }
1165         return this.path.substring(lastSlash + 1, this.path.length());
1166     }
1167 
1168     /**
1169      * Returns the "root" for this URI, if it has one.
1170      * This does not stick close to URI dogma and will
1171      * try to insert the default scheme if there is none,
1172      * and will return null if there is no host or if there
1173      * was an error when the port value was last set. It will
1174      * return null for any opaque URLs as well, as those have
1175      * no host or port.
1176      */
1177     public String getRoot()
1178     {
1179         LinkTool root = root();
1180         if (root == null)
1181         {
1182             return null;
1183         }
1184         return root.toString();
1185     }
1186 
1187     /**
1188      * Returns a new LinkTool instance that represents
1189      * the "root" of the current one, if it has one.
1190      * This essentially calls {@link #absolute()} and
1191      * sets the path, query, and fragment to null on
1192      * the returned instance.
1193      * @see #getRoot()
1194      */
1195     public LinkTool root()
1196     {
1197         if (host == null || opaque || port == -2)
1198         {
1199             return null;
1200         }
1201         LinkTool copy = absolute();
1202         copy.setPath(null);
1203         copy.setQuery(null);
1204         copy.setFragment(null);
1205         return copy;
1206     }
1207 
1208     /**
1209      * Returns a new LinkTool instance with
1210      * the path set to the result of {@link #getDirectory()}
1211      * and the query and fragment set to null.
1212      */
1213     public LinkTool directory()
1214     {
1215         LinkTool copy = root();
1216         if (copy == null)
1217         {
1218             copy = duplicate();
1219             // clear query and fragment, since root() didn't
1220             copy.setQuery(null);
1221             copy.setFragment(null);
1222         }
1223         copy.setPath(getDirectory());
1224         return copy;
1225     }
1226 
1227     /**
1228      * Returns true if this instance is being forced to
1229      * return relative URIs or has a null scheme value.
1230      */
1231     public boolean isRelative()
1232     {
1233         return (this.forceRelative || this.scheme == null);
1234     }
1235 
1236     /**
1237      * Returns a copy of this LinkTool instance that has
1238      * {@link #setForceRelative} set to true.
1239      */
1240     public LinkTool relative()
1241     {
1242         LinkTool copy = duplicate();
1243         copy.setForceRelative(true);
1244         return copy;
1245     }
1246 
1247     /**
1248      * <p>Returns a copy of the link with the specified directory-relative
1249      * URI reference set as the end of the path and {@link #setForceRelative}
1250      * set to true. If the specified relative path is null, that is treated
1251      * the same as an empty path.</p>
1252      *
1253      * Example:<br>
1254      * <code>&lt;a href='$link.relative("/login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
1255      * produces something like</br>
1256      * <code>&lt;a href="/myapp/login/index.vm"&gt;Login Page&lt;/a&gt;</code><br>
1257      *
1258      * @param obj A directory-relative URI reference (e.g. file path in current directory)
1259      * @return a new instance of LinkTool with the specified changes
1260      * @see #relative()
1261      */
1262     public LinkTool relative(Object obj)
1263     {
1264         LinkTool copy = relative();
1265         // prepend relative paths with the current directory
1266         String pth;
1267         if (obj == null)
1268         {
1269             pth = getContextPath();
1270         }
1271         else
1272         {
1273             pth = combinePath(getContextPath(), String.valueOf(obj));
1274         }
1275         copy.setPath(pth);
1276         return copy;
1277     }
1278 
1279     /**
1280      * At this level, this only returns the result of {@link #getDirectory}.
1281      * It is here as an extension hook for subclasses to change the
1282      * "context" for relative links.
1283      * @see #relative(Object)
1284      * @see #getDirectory
1285      */
1286     public String getContextPath()
1287     {
1288         return getDirectory();
1289     }
1290 
1291     /**
1292      * Returns true if this instance has a scheme value
1293      * and is not being forced to create relative URIs.
1294      */
1295     public boolean isAbsolute()
1296     {
1297         return (this.scheme != null && !this.forceRelative);
1298     }
1299 
1300     /**
1301      * Returns a copy of this LinkTool instance that has
1302      * {@link #setForceRelative} set to false and sets the
1303      * scheme to the "http" if no scheme has been set yet.
1304      */
1305     public LinkTool absolute()
1306     {
1307         LinkTool copy = duplicate();
1308         copy.setForceRelative(false);
1309         if (copy.getScheme() == null)
1310         {
1311             copy.setScheme(DEFAULT_SCHEME);
1312         }
1313         return copy;
1314     }
1315 
1316     /**
1317      * <p>Returns a copy of the link with the specified URI reference
1318      * either used as or converted to an absolute (non-relative)
1319      * URI reference. Unless the specified URI contains a query
1320      * or anchor, those values will not be overwritten when using
1321      * this method.</p>
1322      *
1323      * Example:<br>
1324      * <code>&lt;a href='$link.absolute("login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
1325      * produces something like<br/>
1326      * <code>&lt;a href="http://myserver.net/myapp/login/index.vm"&gt;Login Page&lt;/a&gt;</code>;<br>
1327      * <code>&lt;a href='$link.absolute("/login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
1328      * produces something like<br/>
1329      * <code>&lt;a href="http://myserver.net/login/index.vm"&gt;Login Page&lt;/a&gt;</code>;<br>
1330      * and<br>
1331      * <code>&lt;a href='$link.absolute("http://theirserver.com/index.jsp")'&gt;Their, Inc.&lt;/a&gt;</code><br>
1332      * produces something like<br/>
1333      * <code>&lt;a href="http://theirserver.net/index.jsp"&gt;Their, Inc.&lt;/a&gt;</code><br>
1334      *
1335      * @param obj A root-relative or context-relative path or an absolute URI.
1336      * @return a new instance of LinkTool with the specified path or URI
1337      * @see #absolute()
1338      */
1339     public LinkTool absolute(Object obj)
1340     {
1341         // assume it's just a path value to go with current scheme/host/port
1342         LinkTool copy = absolute();
1343         String pth;
1344         if (obj == null)
1345         {
1346             // just use the current directory path, if any
1347             pth = getDirectory();
1348         }
1349         else
1350         {
1351             pth = String.valueOf(obj);
1352             if (pth.startsWith(DEFAULT_SCHEME))
1353             {
1354                 // looks absolute already
1355                 URI uri = toURI(pth);
1356                 if (uri == null)
1357                 {
1358                     return null;
1359                 }
1360                 copy.setScheme(uri.getScheme());
1361                 copy.setUserInfo(uri.getUserInfo());
1362                 copy.setHost(uri.getHost());
1363                 copy.setPort(uri.getPort());
1364                 // handle path, query and fragment with care
1365                 pth = uri.getPath();
1366                 if (pth.equals("/") || pth.length() == 0)
1367                 {
1368                     pth = null;
1369                 }
1370                 copy.setPath(pth);
1371                 if (uri.getQuery() != null)
1372                 {
1373                     copy.setQuery(uri.getQuery());
1374                 }
1375                 if (uri.getFragment() != null)
1376                 {
1377                     copy.setFragment(uri.getFragment());
1378                 }
1379                 return copy;
1380             }
1381             else if (!pth.startsWith("/"))
1382             {
1383                 // paths that don't start with '/'
1384                 // are considered relative to the current directory
1385                 pth = combinePath(getDirectory(), pth);
1386             }
1387         }
1388         copy.setPath(pth);
1389         return copy;
1390     }
1391 
1392     /**
1393      * <p>Returns a copy of the link with the given URI reference set.
1394      * Few changes are applied to the given URI reference. The URI
1395      * reference can be absolute, server-relative, relative and may
1396      * contain query parameters. This method will overwrite all previous
1397      * settings for scheme, host port, path, query and anchor.</p>
1398      *
1399      * @param uri URI reference to set
1400      * @return a new instance of LinkTool
1401      */
1402     public LinkTool uri(Object uri)
1403     {
1404         LinkTool copy = duplicate();
1405         if (copy.setFromURI(uri))
1406         {
1407             return copy;
1408         }
1409         return null;
1410     }
1411 
1412     /**
1413      * If the tool is not in "safe mode"--which it is by default--
1414      * this will return the {@link URI} representation of this instance,
1415      * if any.
1416      * @see SafeConfig#isSafeMode()
1417      */
1418     public URI getUri()
1419     {
1420         if (!isSafeMode())
1421         {
1422             return createURI();
1423         }
1424         return null;
1425     }
1426 
1427     /**
1428      * Returns the full URI of this template without any query data.
1429      * e.g. <code>http://myserver.net/myapp/stuff/View.vm</code>
1430      * Note! The returned String will not represent any URI reference
1431      * or query data set for this LinkTool. A typical application of
1432      * this method is with the HTML base tag. For example:
1433      * <code>&lt;base href="$link.baseRef"&gt;</code>
1434      */
1435     public String getBaseRef()
1436     {
1437         LinkTool copy = duplicate();
1438         copy.setQuery(null);
1439         copy.setFragment(null);
1440         return copy.toString();
1441     }
1442 
1443     /**
1444      * Sets the specified value as the current query data,
1445      * after normalizing the pair delimiters.  This overrides
1446      * any existing query.
1447      */
1448     public LinkTool query(Object query)
1449     {
1450         LinkTool copy = duplicate();
1451         copy.setQuery(query);
1452         return copy;
1453     }
1454 
1455     /**
1456      * Returns the current query as a string, if any.
1457      */
1458     public String getQuery()
1459     {
1460         return toQuery(this.query);
1461     }
1462 
1463     /**
1464      * <p>Adds a key=value pair to the query data. Whether
1465      * this new query pair is appended to the current query
1466      * or overwrites any previous pair(s) with the same key
1467      * is controlled by the {@link #getAppendParams} value.
1468      * The default behavior is to append.</p>
1469      *
1470      * @param key key of new query parameter
1471      * @param value value of new query parameter
1472      * @return a new instance of LinkTool
1473      */
1474     public LinkTool param(Object key, Object value)
1475     {
1476         LinkTool copy = duplicate(true);
1477         copy.setParam(key, value, this.appendParams);
1478         return copy;
1479     }
1480 
1481     /**
1482      * Appends a new key=value pair to the existing query
1483      * data.
1484      *
1485      * @param key key of new query parameter
1486      * @param value value of new query parameter
1487      * @return a new instance of LinkTool
1488      */
1489     public LinkTool append(Object key, Object value)
1490     {
1491         LinkTool copy = duplicate(true);
1492         copy.setParam(key, value, true);
1493         return copy;
1494     }
1495 
1496     /**
1497      * Sets a new key=value pair to the existing query
1498      * data, overwriting any previous pair(s) that have
1499      * the same key.
1500      *
1501      * @param key key of new query parameter
1502      * @param value value of new query parameter
1503      * @return a new instance of LinkTool
1504      */
1505     public LinkTool set(Object key, Object value)
1506     {
1507         LinkTool copy = duplicate(true);
1508         copy.setParam(key, value, false);
1509         return copy;
1510     }
1511 
1512     /**
1513      * Returns a new LinkTool instance that has any
1514      * value(s) under the specified key removed from the query data.
1515      *
1516      * @param key key of the query pair(s) to be removed
1517      * @return a new instance of LinkTool
1518      */
1519     public LinkTool remove(Object key)
1520     {
1521         LinkTool copy = duplicate(true);
1522         copy.removeParam(key);
1523         return copy;
1524     }
1525 
1526     /**
1527      * This method can do two different things.  If you pass in a
1528      * boolean, it will create a new LinkTool duplicate and call
1529      * {@link #handleParamsBoolean(boolean)} on it. In this class, true
1530      * values do nothing (subclasses may have use for them), but false
1531      * values will clear out all params in the query for that instance.
1532      * If you pass in a query string or a Map of parameters, those
1533      * values will be added to the new LinkTool, either overwriting
1534      * previous value(s) with those keys or appending to them,
1535      * depending on the {@link #getAppendParams} value.
1536      *
1537      * @param parameters a boolean or new query data (either Map or query string)
1538      * @return a new instance of LinkTool
1539      */
1540     public LinkTool params(Object parameters)
1541     {
1542         // don't waste time with null/empty data
1543         if (parameters == null)
1544         {
1545             return this;
1546         }
1547         if (parameters instanceof Boolean)
1548         {
1549             Boolean action = ((Boolean)parameters).booleanValue();
1550             LinkTool copy = duplicate(true);
1551             copy.handleParamsBoolean(action);
1552             return copy;
1553         }
1554         if (parameters instanceof Map && ((Map)parameters).isEmpty())
1555         {
1556             return duplicate(false);
1557         }
1558 
1559         LinkTool copy = duplicate(this.appendParams);
1560         copy.setParams(parameters, this.appendParams);
1561         return copy;
1562     }
1563 
1564     public Map getParams()
1565     {
1566         if (this.query == null || this.query.isEmpty())
1567         {
1568             return null;
1569         }
1570         return this.query;
1571     }
1572 
1573     /**
1574      * <p>Returns a copy of the link with the specified anchor to be
1575      *    added to the end of the generated hyperlink.</p>
1576      *
1577      * Example:<br>
1578      * <code>&lt;a href='$link.setAnchor("foo")'&gt;Foo&lt;/a&gt;</code><br>
1579      * produces something like</br>
1580      * <code>&lt;a href="#foo"&gt;Foo&lt;/a&gt;</code><br>
1581      *
1582      * @param anchor an internal document reference
1583      * @return a new instance of LinkTool with the set anchor
1584      */
1585     public LinkTool anchor(Object anchor)
1586     {
1587         LinkTool copy = duplicate();
1588         copy.setFragment(anchor);
1589         return copy;
1590     }
1591 
1592     /**
1593      * Returns the anchor (internal document reference) set for this link.
1594      */
1595     public String getAnchor()
1596     {
1597         return this.fragment;
1598     }
1599 
1600     public LinkTool getSelf()
1601     {
1602         // there are no self-params to bother with at this level,
1603         return self;
1604     }
1605 
1606     /**
1607      * Returns the full URI reference that's been built with this tool,
1608      * including the query string and anchor, e.g.
1609      * <code>http://myserver.net/myapp/stuff/View.vm?id=42&type=blue#foo</code>.
1610      * Typically, it is not necessary to call this method explicitely.
1611      * Velocity will call the toString() method automatically to obtain
1612      * a representable version of an object.
1613      */
1614     public String toString()
1615     {
1616         URI uri = createURI();
1617         if (uri == null)
1618         {
1619             return null;
1620         }
1621         if (query != null)
1622         {
1623             return decodeQueryPercents(uri.toString());
1624         }
1625         return uri.toString();
1626     }
1627 
1628     /**
1629      * This is an ugly (but fast) hack that's needed because URI encodes
1630      * things that we don't need encoded while not encoding things
1631      * that we do need encoded.  So, we have to encode query data
1632      * before creating the URI to ensure they are properly encoded,
1633      * but then URI encodes all the % from that encoding.  Here,
1634      * we isolate the query data and manually decode the encoded
1635      * %25 in that section back to %, without decoding anything else.
1636      */
1637     protected String decodeQueryPercents(String url)
1638     {
1639         StringBuilder out = new StringBuilder(url.length());
1640         boolean inQuery = false, havePercent = false, haveTwo = false;
1641         for (int i=0; i<url.length(); i++)
1642         {
1643             char c = url.charAt(i);
1644             if (inQuery)
1645             {
1646                 if (havePercent)
1647                 {
1648                     if (haveTwo)
1649                     {
1650                         out.append('%');
1651                         if (c != '5')
1652                         {
1653                             out.append('2').append(c);
1654                         }
1655                         havePercent = haveTwo = false;
1656                     }
1657                     else if (c == '2')
1658                     {
1659                         haveTwo = true;
1660                     }
1661                     else
1662                     {
1663                         out.append('%').append(c);
1664                         havePercent = false;
1665                     }
1666                 }
1667                 else if (c == '%')
1668                 {
1669                     havePercent = true;
1670                 }
1671                 else
1672                 {
1673                     out.append(c);
1674                 }
1675                 if (c == '#')
1676                 {
1677                     inQuery = false;
1678                 }
1679             }
1680             else
1681             {
1682                 out.append(c);
1683                 if (c == '?')
1684                 {
1685                     inQuery = true;
1686                 }
1687             }
1688         }
1689         // if things ended part way
1690         if (havePercent)
1691         {
1692             out.append('%');
1693             if (haveTwo)
1694             {
1695                 out.append('2');
1696             }
1697         }
1698         return out.toString();
1699     }
1700 
1701     /**
1702      * This instance is considered equal to any
1703      * LinkTool instance whose toString() method returns a
1704      * String equal to that returned by this instance's toString()
1705      * @see #toString()
1706      */
1707     @Override
1708     public boolean equals(Object obj)
1709     {
1710         if (obj == null || !(obj instanceof LinkTool))
1711         {
1712             return false;
1713         }
1714         // string value is all that ultimately matters
1715         String that = obj.toString();
1716         if (that == null && toString() == null)
1717         {
1718             return true;
1719         }
1720         return that.equals(toString());
1721     }
1722 
1723     /**
1724      * Returns the hash code for the result of toString().
1725      * If toString() returns {@code null} (yes, we do break that contract),
1726      * this will return {@code -1}.
1727      */
1728     @Override
1729     public int hashCode()
1730     {
1731         String hashme = toString();
1732         if (hashme == null)
1733         {
1734             return -1;
1735         }
1736         return hashme.hashCode();
1737     }
1738 
1739 
1740     /**
1741      * Delegates encoding of the specified url content to
1742      * {@link URLEncoder#encode} using the configured character encoding.
1743      *
1744      * @return String - the encoded url.
1745      */
1746     public String encode(Object obj)
1747     {
1748         if (obj == null)
1749         {
1750             return null;
1751         }
1752         try
1753         {
1754             return URLEncoder.encode(String.valueOf(obj), charset);
1755         }
1756         catch (UnsupportedEncodingException uee)
1757         {
1758             debug("Character encoding '%s' is unsupported", uee, charset);
1759             return null;
1760         }
1761     }
1762 
1763     /**
1764      * Delegates decoding of the specified url content to
1765      * {@link URLDecoder#decode} using the configured character encoding.
1766      *
1767      * @return String - the decoded url.
1768      */
1769     public String decode(Object obj)
1770     {
1771         if (obj == null)
1772         {
1773             return null;
1774         }
1775         try
1776         {
1777             return URLDecoder.decode(String.valueOf(obj), charset);
1778         }
1779         catch (UnsupportedEncodingException uee)
1780         {
1781             debug("Character encoding '%s' is unsupported", uee, charset);
1782             return null;
1783         }
1784     }
1785 
1786 }