You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by nb...@apache.org on 2008/10/10 06:47:35 UTC

svn commit: r703317 - in /velocity/tools/trunk/src: main/java/org/apache/velocity/tools/generic/LinkTool.java test/java/org/apache/velocity/tools/LinkToolTests.java

Author: nbubna
Date: Thu Oct  9 21:47:32 2008
New Revision: 703317

URL: http://svn.apache.org/viewvc?rev=703317&view=rev
Log:
greatly improve parameter handling, make absolute/relative stuff work sanely, and generally improve the consistency and usefulness of this generic LinkTool version

Modified:
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java
    velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java

Modified: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java?rev=703317&r1=703316&r2=703317&view=diff
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java (original)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java Thu Oct  9 21:47:32 2008
@@ -87,6 +87,8 @@
     /** XHTML delimiter for query data ('&') */
     public static final String XHTML_QUERY_DELIMITER = "&";
 
+    public static final String APPEND_PARAMS_KEY = "appendParameters";
+    public static final String FORCE_RELATIVE_KEY = "forceRelative";
     public static final String DEFAULT_CHARSET = "UTF-8";
     public static final String DEFAULT_SCHEME = "http";
     public static final String SECURE_SCHEME = "https";
@@ -108,10 +110,12 @@
     protected String host;
     protected int port;
     protected String path;
-    protected String query;
+    protected Map query;
     protected String fragment;
     protected String charset;
     protected String queryDelim;
+    protected boolean appendParams;
+    protected boolean forceRelative;
 
     private boolean opaque;
     private final LinkTool self;
@@ -132,6 +136,8 @@
         charset = DEFAULT_CHARSET;
         queryDelim = XHTML_QUERY_DELIMITER;
         opaque = false;
+        appendParams = true;
+        forceRelative = false;
         self = this;
     }
 
@@ -203,6 +209,16 @@
         {
             setXHTML(xhtml);
         }
+        Boolean addParams = props.getBoolean(APPEND_PARAMS_KEY);
+        if (addParams != null)
+        {
+            setAppendParams(addParams);
+        }
+        Boolean forceRelative = props.getBoolean(FORCE_RELATIVE_KEY);
+        if (forceRelative != null)
+        {
+            setForceRelative(forceRelative);
+        }
     }
 
     /**
@@ -212,9 +228,25 @@
      */
     protected LinkTool duplicate()
     {
+        return duplicate(false);
+    }
+
+    /**
+     * Equivalent to clone, but with no checked exceptions.
+     * If for some unfathomable reason clone() doesn't work,
+     * this will throw a RuntimeException.  If doing a deep
+     * clone, then the parameter Map will also be cloned.
+     */
+    protected LinkTool duplicate(boolean deep)
+    {
         try
         {
-            return (LinkTool)this.clone();
+            LinkTool that = (LinkTool)this.clone();
+            if (deep && query != null)
+            {
+                that.query = new LinkedHashMap(query);
+            }
+            return that;
         }
         catch (CloneNotSupportedException e)
         {
@@ -250,6 +282,29 @@
     }
 
     /**
+     * Sets whether or not the {@link #setParam(Object,Object)} method
+     * will override existing query values for the same key or simply append
+     * the new value to a list of existing values.
+     */
+    public void setAppendParams(boolean addParams)
+    {
+        this.appendParams = addParams;
+    }
+
+    /**
+     * Sets whether or not the {@link #createURI} method should ignore the
+     * scheme, user, port and host values for non-opaque URIs, thus making
+     * {@link #toString} print the link as a relative one, not an absolute
+     * one.  NOTE: using {@link #absolute()}, {@link #absolute(Object)},
+     * {@link #relative()}, or {@link #relative(Object)} will alter this
+     * setting accordingly on the new instances they return.
+     */
+    public void setForceRelative(boolean forceRelative)
+    {
+        this.forceRelative = forceRelative;
+    }
+
+    /**
      * This will treat empty strings like null values
      * and will trim any trailing ':' character.
      */
@@ -382,12 +437,11 @@
     }
 
     /**
-     * This will trim any '?' character at the start of the
-     * specified value.  It will also check the value to see if
-     * it already includes multiple query pairs by checking for
-     * any '&' characters in the string.  If there are some, then
-     * the delimiters between the pairs will all be replaced
-     * with this instance's configured query delimiter.
+     * If the specified value is null, it will set the query to null.
+     * If a Map, it will copy all those values into a new LinkedHashMap and
+     * replace any current query value with that. If it is a String,
+     * it will use {@link #parseQuery(String)} to parse it into a map
+     * of keys to values.
      */
     public void setQuery(Object obj)
     {
@@ -395,30 +449,38 @@
         {
             this.query = null;
         }
+        else if (obj instanceof Map)
+        {
+            this.query = new LinkedHashMap((Map)obj);
+        }
         else
         {
-            this.query = String.valueOf(obj);
-            if (query.startsWith("?"))
-            {
-                this.query = query.substring(1, query.length());
-            }
-            // if we have multiple pairs...
-            if (query.contains("&"))
-            {
-                // ensure the delimeters match the xhtml setting
-                // this impl is not at all efficient, but it's easy
-                this.query = query.replaceAll("&(amp;)?", queryDelim);
-            }
+            String qs = normalizeQuery(String.valueOf(obj));
+            this.query = parseQuery(qs);
+        }
+    }
+
+    protected String normalizeQuery(String qs)
+    {
+        // if we have multiple pairs...
+        if (qs.indexOf('&') >= 0)
+        {
+            // ensure the delimeters match the xhtml setting
+            // this impl is not at all efficient, but it's easy
+            qs = qs.replaceAll("&(amp;)?", queryDelim);
         }
+        return qs;
     }
 
     /**
-     * Converts the map of keys to values into a query string
-     * and uses {@link #appendQuery(Object)} to add it to the
-     * current {@link #getQuery} value.
+     * Converts the map of keys to values into a query string.
      */
-    public void appendQuery(Map parameters)
+    public String toQuery(Map parameters)
     {
+        if (parameters == null)
+        {
+            return null;
+        }
         StringBuilder query = new StringBuilder();
         for (Object e : parameters.entrySet())
         {
@@ -430,7 +492,7 @@
             }
             query.append(toQuery(entry.getKey(), entry.getValue()));
         }
-        appendQuery(query);
+        return query.toString();
     }
 
     /**
@@ -446,6 +508,140 @@
     }
 
     /**
+     * If there is no existing value for this key in the query, it
+     * will simply add it and its value to the query.  If the key
+     * already is present in the query and append
+     * is true, this will add the specified value to those
+     * already under that key.  If {@link #appendParams} is
+     * false, this will override the existing values with the
+     * specified new value.
+     */
+    public void setParam(Object key, Object value, boolean append)
+    {
+        // use all keys as strings, even null -> "null"
+        key = String.valueOf(key);
+        if (this.query == null)
+        {
+            this.query = new LinkedHashMap();
+            query.put(key, value);
+        }
+        else if (append)
+        {
+            appendParam((String)key, value);
+        }
+        else
+        {
+            query.put(key, value);
+        }
+    }
+
+    private void appendParam(String key, Object value)
+    {
+        if (query.containsKey(key))
+        {
+            Object cur = query.get(key);
+            if (cur instanceof List)
+            {
+                addToList((List)cur, value);
+            }
+            else
+            {
+                List vals = new ArrayList();
+                vals.add(cur);
+                addToList(vals, value);
+                query.put(key, vals);
+            }
+        }
+        else
+        {
+            query.put(key, value);
+        }
+    }
+
+    private void addToList(List vals, Object value)
+    {
+        if (value instanceof List)
+        {
+            for (Object v : ((List)value))
+            {
+                vals.add(v);
+            }
+        }
+        else if (value instanceof Object[])
+        {
+            for (Object v : ((Object[])value))
+            {
+                vals.add(v);
+            }
+        }
+        else
+        {
+            vals.add(value);
+        }
+    }
+
+    /**
+     * If append is false, this simply delegates to {@link #setQuery}.
+     * Otherwise, if the specified object is null, it does nothing.  If the object
+     * is not a Map, it will turn it into a String and use {@link #parseQuery} to
+     * parse it. Once it is a Map, it will iterate through the entries appending
+     * each key/value to the current query data.
+     */
+    public void setParams(Object obj, boolean append)
+    {
+        if (!append)
+        {
+            setQuery(obj);
+        }
+        else if (obj != null)
+        {
+            if (!(obj instanceof Map))
+            {
+                obj = parseQuery(String.valueOf(obj));
+            }
+            if (obj != null)
+            {
+                if (query == null)
+                {
+                    this.query = new LinkedHashMap();
+                }
+                for (Object e : ((Map)obj).entrySet())
+                {
+                    Map.Entry entry = (Map.Entry)e;
+                    String key = String.valueOf(entry.getKey());
+                    appendParam(key, entry.getValue());
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes the query pair(s) with the specified key from the
+     * query data and returns the remove value(s), if any.
+     */
+    public Object removeParam(Object key)
+    {
+        if (query != null)
+        {
+            key = String.valueOf(key);
+            return query.remove(key);
+        }
+        return null;
+    }
+
+    /**
+     * In this class, this method ignores true values.  If passed a false value,
+     * it will call {@link #setQuery} with a null value to clear all query data.
+     */
+    protected void handleParamsBoolean(boolean keep)
+    {
+        if (!keep)
+        {
+            setQuery(null);
+        }
+    }
+
+    /**
      * If the second param is null or empty, this will simply return the first
      * and vice versa.  Otherwise, it will trim any '?'
      * at the start of the second param and any '&' or '&' at the
@@ -538,12 +734,14 @@
     }
 
     /**
-     * Uses {@link #parseQuery(String,String)} to parse the specified
-     * query string using this instance's current query delimiter.
+     * Uses {@link #normalizeQuery} to make all delimiters in the
+     * specified query string match the current query delimiter 
+     * and then uses {@link #parseQuery(String,String)} to parse it
+     * according to that same delimiter.
      */
     protected Map<String,Object> parseQuery(String query)
     {
-        return parseQuery(query, this.queryDelim);
+        return parseQuery(normalizeQuery(query), this.queryDelim);
     }
 
     /**
@@ -632,22 +830,10 @@
             return true;
         }
 
-        URI uri;
-        if (obj instanceof URI)
-        {
-            uri = (URI)obj;
-        }
-        else
+        URI uri = toURI(obj);
+        if (uri == null)
         {
-            try
-            {
-                uri = new URI(String.valueOf(obj));
-            }
-            catch (Exception e)
-            {
-                debug("Could convert '%s' to URI", e, obj);
-                return false;
-            }
+            return false;
         }
         setScheme(uri.getScheme());
         if (uri.isOpaque())
@@ -674,6 +860,29 @@
     }
 
     /**
+     * Turns the specified object into a string and thereby a URI.
+     */
+    protected URI toURI(Object obj)
+    {
+        if (obj instanceof URI)
+        {
+            return (URI)obj;
+        }
+        else
+        {
+            try
+            {
+                return new URI(String.valueOf(obj));
+            }
+            catch (Exception e)
+            {
+                debug("Could convert '%s' to URI", e, obj);
+                return null;
+            }
+        }
+    }
+
+    /**
      * Tries to create a URI from the current port, opacity, scheme,
      * userInfo, host, path, query and fragment set for this instance,
      * using the {@link URI} constructor that is appropriate to the opacity.
@@ -695,6 +904,14 @@
                     // path is used as scheme-specific part
                     return new URI(scheme, path, anchor);
                 }
+                else if (forceRelative)
+                {
+                    if (path == null && query == null && fragment == null)
+                    {
+                        return null;
+                    }
+                    return new URI(null, null, null, -1, path, toQuery(query), anchor);
+                }
                 else
                 {
                     // only create the URI if we have some values besides a port
@@ -703,7 +920,7 @@
                     {
                         return null;
                     }
-                    return new URI(scheme, user, host, port, path, query, fragment);
+                    return new URI(scheme, user, host, port, path, toQuery(query), anchor);
                 }
             }
         }
@@ -735,6 +952,15 @@
         return queryDelim.equals(XHTML_QUERY_DELIMITER);
     }
 
+    /**
+     * Returns true if {@link #param(Object,Object)} appends values;
+     * false if the method overwrites existing value(s) for the specified key.
+     */
+    public boolean getAppendParams()
+    {
+        return this.appendParams;
+    }
+
 
     /**
      * Returns a new instance with the specified value set as its scheme.
@@ -779,18 +1005,6 @@
     }
 
     /**
-     * Returns true if this instance has a scheme value.
-     */
-    public boolean isAbsolute()
-    {
-        if (this.scheme == null)
-        {
-            return false;
-        }
-        return true;
-    }
-
-    /**
      * Returns true if this instance represents an opaque URI.
      * @see URI
      */
@@ -902,40 +1116,40 @@
     }
 
     /**
-     * At this level, this method returns all "directories"
+     * Returns the directory stack
      * in the set {@link #getPath()} value, by just trimming
-     * of the last "/" and all that follows.
+     * off all that follows the last "/".
      */
-    public String getContextPath()
+    public String getDirectory()
     {
         if (this.path == null || this.opaque)
         {
             return null;
         }
         int lastSlash = this.path.lastIndexOf('/');
-        if (lastSlash <= 0)
+        if (lastSlash < 0)
         {
             return "";
         }
-        return this.path.substring(0, lastSlash);
+        return this.path.substring(0, lastSlash + 1);
     }
 
     /**
-     * At this level, this method returns the last section
-     * of the path, from the final "/" onward.
+     * Returns the last section of the path,
+     * which is all that follows the final "/".
      */
-    public String getRequestPath()
+    public String getFile()
     {
         if (this.path == null || this.opaque)
         {
             return null;
         }
         int lastSlash = this.path.lastIndexOf('/');
-        if (lastSlash <= 0)
+        if (lastSlash < 0)
         {
             return this.path;
         }
-        return this.path.substring(lastSlash, this.path.length());
+        return this.path.substring(lastSlash + 1, this.path.length());
     }
 
     /**
@@ -949,117 +1163,198 @@
      */
     public String getRoot()
     {
-        if (host == null || opaque || port == -2)
+        LinkTool root = root();
+        if (root == null)
         {
             return null;
         }
-        if (scheme == null)
-        {
-            scheme = DEFAULT_SCHEME;
-        }
-        StringBuilder out = new StringBuilder();
-        out.append(scheme);
-        out.append("://");
-        out.append(host);
-        // if we have a port that's not a default for the scheme
-        if (port >= 0 &&
-            ((scheme.equals(DEFAULT_SCHEME) && port != 80) ||
-            (isSecure() && port != 443)))
+        return root.toString();
+    }
+
+    /**
+     * Returns a new LinkTool instance that represents
+     * the "root" of the current one, if it has one.
+     * This essentially calls {@link #absolute()} and
+     * sets the path, query, and fragment to null on
+     * the returned instance.
+     * @see #getRoot()
+     */
+    public LinkTool root()
+    {
+        if (host == null || opaque || port == -2)
         {
-            out.append(':');
-            out.append(port);
+            return null;
         }
-        return out.toString();
+        LinkTool copy = absolute();
+        copy.setPath(null);
+        copy.setQuery(null);
+        copy.setFragment(null);
+        return copy;
     }
-        
 
     /**
-     * <p>Returns the URI that addresses the current "directory"
-     * of this instance. This string does not end with a "/" and
-     * is essentially a concatenation of {@link #getRoot} and
-     * {@link #getContextPath}</p>
+     * Returns a new LinkTool instance with
+     * the path set to the result of {@link #getDirectory()}
+     * and the query and fragment set to null.
      */
-    public String getContextURL()
+    public LinkTool directory()
     {
-        String root = getRoot();
-        if (root == null)
+        LinkTool copy = root();
+        if (copy == null)
         {
-            return null;
+            copy = duplicate();
+            // clear query and fragment, since root() didn't
+            copy.setQuery(null);
+            copy.setFragment(null);
         }
-        return combinePath(root, getContextPath());
+        copy.setPath(getDirectory());
+        return copy;
     }
 
     /**
-     * <p>Returns a copy of the link with the specified context-relative
-     * URI reference converted to a server-relative URI reference. This
-     * method will overwrite any previous URI reference settings but will
-     * copy the query string.</p>
+     * Returns true if this instance is being forced to
+     * return relative URIs or has a null scheme value.
+     */
+    public boolean isRelative()
+    {
+        return (this.forceRelative || this.scheme == null);
+    }
+
+    /**
+     * Returns a copy of this LinkTool instance that has
+     * {@link #setForceRelative} set to true.
+     */
+    public LinkTool relative()
+    {
+        LinkTool copy = duplicate();
+        copy.setForceRelative(true);
+        return copy;
+    }
+
+    /**
+     * <p>Returns a copy of the link with the specified directory-relative
+     * URI reference set as the end of the path and {@link #setForceRelative}
+     * set to true.</p>
      *
      * Example:<br>
-     * <code>&lt;a href='$link.relative("/templates/login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
+     * <code>&lt;a href='$link.relative("/login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
      * produces something like</br>
-     * <code>&lt;a href="/myapp/templates/login/index.vm"&gt;Login Page&lt;/a&gt;</code><br>
+     * <code>&lt;a href="/myapp/login/index.vm"&gt;Login Page&lt;/a&gt;</code><br>
      *
-     * @param obj A context-relative URI reference. A context-relative URI
-     *        is a URI that is relative to the root of this web application.
-     * @return a new instance of LinkTool with the specified URI
+     * @param obj A directory-relative URI reference (e.g. file path in current directory)
+     * @return a new instance of LinkTool with the specified changes
+     * @see #relative()
      */
     public LinkTool relative(Object obj)
     {
         if (obj == null)
         {
-            return path(getContextPath());
+            return path(getDirectory());
         }
         String pth = String.valueOf(obj);
+        LinkTool copy = relative();
+        // prepend relative paths with the current directory
+        copy.setPath(combinePath(getDirectory(), pth));
+        return copy;
+    }
+
+    /**
+     * Returns true if this instance has a scheme value
+     * and is not being forced to create relative URIs.
+     */
+    public boolean isAbsolute()
+    {
+        return (this.scheme != null && !this.forceRelative);
+    }
+
+    /**
+     * Returns a copy of this LinkTool instance that has
+     * {@link #setForceRelative} set to false and sets the
+     * scheme to the "http" if no scheme has been set yet.
+     */
+    public LinkTool absolute()
+    {
         LinkTool copy = duplicate();
-        // prepend relative paths with context path
-        copy.setPath(combinePath(getContextPath(), pth));
+        copy.setForceRelative(false);
+        if (copy.getScheme() == null)
+        {
+            copy.setScheme(DEFAULT_SCHEME);
+        }
         return copy;
     }
 
     /**
      * <p>Returns a copy of the link with the specified URI reference
      * either used as or converted to an absolute (non-relative)
-     * URI reference. This method will overwrite any previous URI
-     * reference settings but will copy the query string.</p>
+     * URI reference. Unless the specified URI contains a query
+     * or anchor, those values will not be overwritten when using
+     * this method.</p>
      *
      * Example:<br>
-     * <code>&lt;a href='$link.absolute("/templates/login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
+     * <code>&lt;a href='$link.absolute("login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
      * produces something like<br/>
-     * <code>&lt;a href="http://myserver.net/myapp/templates/login/index.vm"&gt;Login Page&lt;/a&gt;</code><br>
+     * <code>&lt;a href="http://myserver.net/myapp/login/index.vm"&gt;Login Page&lt;/a&gt;</code>;<br>
+     * <code>&lt;a href='$link.absolute("/login/index.vm")'&gt;Login Page&lt;/a&gt;</code><br>
+     * produces something like<br/>
+     * <code>&lt;a href="http://myserver.net/login/index.vm"&gt;Login Page&lt;/a&gt;</code>;<br>
      * and<br>
      * <code>&lt;a href='$link.absolute("http://theirserver.com/index.jsp")'&gt;Their, Inc.&lt;/a&gt;</code><br>
      * produces something like<br/>
      * <code>&lt;a href="http://theirserver.net/index.jsp"&gt;Their, Inc.&lt;/a&gt;</code><br>
      *
-     * @param obj A context-relative URI reference or absolute URL.
-     * @return a new instance of LinkTool with the specified URI
-     * @see #uri(Object uri)
-     * @since VelocityTools 1.3
+     * @param obj A root-relative or context-relative path or an absolute URI.
+     * @return a new instance of LinkTool with the specified path or URI
+     * @see #absolute()
      */
     public LinkTool absolute(Object obj)
     {
+        // assume it's just a path value to go with current scheme/host/port
+        LinkTool copy = absolute();
+        String pth;
         if (obj == null)
         {
-            // use uri's null handling
-            return uri(obj);
+            // just use the current directory path, if any
+            pth = getDirectory();
         }
-        String pth = String.valueOf(obj);
-        if (pth.startsWith(DEFAULT_SCHEME))
-        {
-            // looks absolute already
-            return uri(pth);
-        }
-
-        // assume it's relative and try to make it absolute
-        String root = getRoot();
-        if (root != null)
+        else
         {
-            return uri(combinePath(root, pth));
+            pth = String.valueOf(obj);
+            if (pth.startsWith(DEFAULT_SCHEME))
+            {
+                // looks absolute already
+                URI uri = toURI(pth);
+                if (uri == null)
+                {
+                    return null;
+                }
+                copy.setScheme(uri.getScheme());
+                copy.setUserInfo(uri.getUserInfo());
+                copy.setHost(uri.getHost());
+                copy.setPort(uri.getPort());
+                // handle path, query and fragment with care
+                pth = uri.getPath();
+                if (pth.equals("/") || pth.length() == 0)
+                {
+                    pth = null;
+                }
+                copy.setPath(pth);
+                if (uri.getQuery() != null)
+                {
+                    copy.setQuery(uri.getQuery());
+                }
+                if (uri.getFragment() != null)
+                {
+                    copy.setFragment(uri.getFragment());
+                }
+                return copy;
+            }
+            else if (!pth.startsWith("/"))
+            {
+                // paths that don't start with '/'
+                // are considered relative to the current directory
+                pth = combinePath(getDirectory(), pth);
+            }
         }
-        // ugh. this is about all we can do here w/o a host
-        LinkTool copy = duplicate();
-        copy.setScheme(DEFAULT_SCHEME);
         copy.setPath(pth);
         return copy;
     }
@@ -1073,7 +1368,6 @@
      *
      * @param uri URI reference to set
      * @return a new instance of LinkTool
-     * @since VelocityTools 1.3
      */
     public LinkTool uri(Object uri)
     {
@@ -1117,8 +1411,9 @@
     }
 
     /**
-     * Sets the specified value as the current query string,
-     * after normalizing the pair delimiters.
+     * Sets the specified value as the current query data,
+     * after normalizing the pair delimiters.  This overrides
+     * any existing query.
      */
     public LinkTool query(Object query)
     {
@@ -1128,60 +1423,121 @@
     }
 
     /**
-     * Returns the current query string, if any.
+     * Returns the current query as a string, if any.
      */
     public String getQuery()
     {
-        return this.query;
+        return toQuery(this.query);
     }
 
     /**
-     * <p>Adds a key=value pair to the query data. This returns a new LinkTool
-     * containing both a copy of this LinkTool's query data and the new data.
-     * Query data is URL encoded before it is appended.</p>
+     * <p>Adds a key=value pair to the query data. Whether
+     * this new query pair is appended to the current query
+     * or overwrites any previous pair(s) with the same key
+     * is controlled by the {@link #getAppendsParam} value.
+     * The default behavior is to append.</p>
      *
      * @param key key of new query parameter
      * @param value value of new query parameter
-     *
      * @return a new instance of LinkTool
-     * @since VelocityTools 1.3
      */
     public LinkTool param(Object key, Object value)
     {
-        LinkTool copy = duplicate();
-        copy.appendQuery(toQuery(key, value));
+        LinkTool copy = duplicate(true);
+        copy.setParam(key, value, this.appendParams);
         return copy;
     }
 
     /**
-     * <p>Adds multiple key=value pairs to the query data.
-     * This returns a new LinkTool containing both a copy of
-     * this LinkTool's query data and the new data.
-     * Query data is URL encoded before it is appended.</p>
+     * Appends a new key=value pair to the existing query
+     * data.
      *
-     * @param parameters map of new query data keys to values
+     * @param key key of new query parameter
+     * @param value value of new query parameter
      * @return a new instance of LinkTool
-     * @since VelocityTools 1.3
      */
-    public LinkTool params(Map parameters)
+    public LinkTool append(Object key, Object value)
+    {
+        LinkTool copy = duplicate(true);
+        copy.setParam(key, value, true);
+        return copy;
+    }
+
+    /**
+     * Sets a new key=value pair to the existing query
+     * data, overwriting any previous pair(s) that have
+     * the same key.
+     *
+     * @param key key of new query parameter
+     * @param value value of new query parameter
+     * @return a new instance of LinkTool
+     */
+    public LinkTool set(Object key, Object value)
+    {
+        LinkTool copy = duplicate(true);
+        copy.setParam(key, value, false);
+        return copy;
+    }
+
+    /**
+     * Returns a new LinkTool instance that has any
+     * value(s) under the specified key removed from the query data.
+     *
+     * @param key key of the query pair(s) to be removed
+     * @return a new instance of LinkTool
+     */
+    public LinkTool remove(Object key)
+    {
+        LinkTool copy = duplicate(true);
+        copy.removeParam(key);
+        return copy;
+    }
+
+    /**
+     * This method can do two different things.  If you pass in a
+     * boolean, it will create a new LinkTool duplicate and call
+     * {@link #handleParamsBoolean(boolean)} on it. In this class, true
+     * values do nothing (subclasses may have use for them), but false
+     * values will clear out all params in the query for that instance.
+     * If you pass in a query string or a Map of parameters, those
+     * values will be added to the new LinkTool, either overwriting
+     * previous value(s) with those keys or appending to them,
+     * depending on the {@link #getAppendParams} value.
+     *
+     * @param parameters a boolean or new query data (either Map or query string)
+     * @return a new instance of LinkTool
+     */
+    public LinkTool params(Object parameters)
     {
         // don't waste time with null/empty data
-        if (parameters == null || parameters.isEmpty())
+        if (parameters == null)
         {
             return this;
         }
-        LinkTool copy = duplicate();
-        copy.appendQuery(parameters);
+        if (parameters instanceof Boolean)
+        {
+            Boolean action = ((Boolean)parameters).booleanValue();
+            LinkTool copy = duplicate(true);
+            copy.handleParamsBoolean(action);
+            return copy;
+        }
+        if (parameters instanceof Map && ((Map)parameters).isEmpty())
+        {
+            return duplicate(false);
+        }
+
+        LinkTool copy = duplicate(this.appendParams);
+        copy.setParams(parameters, this.appendParams);
         return copy;
     }
 
     public Map getParams()
     {
-        if (this.query == null || this.query.length() == 0)
+        if (this.query == null || this.query.isEmpty())
         {
             return null;
         }
-        return parseQuery(this.query);
+        return this.query;
     }
 
     /**
@@ -1195,7 +1551,6 @@
      *
      * @param anchor an internal document reference
      * @return a new instance of LinkTool with the set anchor
-     * @since VelocityTools 1.3
      */
     public LinkTool anchor(Object anchor)
     {

Modified: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java?rev=703317&r1=703316&r2=703317&view=diff
==============================================================================
--- velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java (original)
+++ velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java Thu Oct  9 21:47:32 2008
@@ -105,11 +105,23 @@
         assertSame(link.getUser(), result.getUser());
         assertSame(link.getHost(), result.getHost());
         assertSame(link.getPath(), result.getPath());
-        assertSame(link.getQuery(), result.getQuery());
+        assertSame(link.query, result.query);
         assertSame(link.getAnchor(), result.getAnchor());
         assertSame(link.getSelf(), result.getSelf());
     }
 
+    public @Test void methodDuplicate_boolean() throws Exception
+    {
+        LinkTool link = newInstance("http://apache.org/foo.html?foo=bar");
+        LinkTool result = link.duplicate(true);
+        assertFalse(link == result);
+        assertSame(link.getPath(), result.getPath());
+        assertFalse(link.query == result.query);
+        assertEquals(link.getQuery(), result.getQuery());
+        assertSame(link.getSelf(), result.getSelf());
+        assertSame(result.query, result.duplicate(false).query);
+    }
+
     public @Test void methodEncode_Object() throws Exception
     {
         LinkTool link = newInstance();
@@ -337,20 +349,26 @@
         assertEquals("/bar/foo", link.append("foo").getPath());
     }
 
-    public @Test void methodGetContextPath() throws Exception
+    public @Test void methodGetDirectory() throws Exception
     {
         LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
-        assertEquals("/ctx", link.getContextPath());
-        link = newInstance("http://foo.com/foo/bar/request.vm?this=that#anc");
-        assertEquals("/foo/bar", link.getContextPath());
+        assertEquals("/ctx/", link.getDirectory());
+        link = newInstance("http://foo.com");
+        assertNull(link.getDirectory());
+        link = newInstance("http://foo.com/bar");
+        assertEquals("/", link.getDirectory());
+        link = newInstance("http://foo.com/bar/foo/bar/foo");
+        assertEquals("/bar/foo/bar/", link.getDirectory());
     }
 
-    public @Test void methodGetRequestPath() throws Exception
+    public @Test void methodGetFile() throws Exception
     {
         LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
-        assertEquals("/request.vm", link.getRequestPath());
+        assertEquals("request.vm", link.getFile());
         link = newInstance("http://foo.com/foo/bar/request.vm?this=that#anc");
-        assertEquals("/request.vm", link.getRequestPath());
+        assertEquals("request.vm", link.getFile());
+        link = newInstance("http://foo.com/bar/");
+        assertEquals("", link.getFile());
     }
 
     public @Test void methodGetRoot() throws Exception
@@ -361,37 +379,64 @@
         assertEquals("https://apache.org", link.secure().getRoot());
     }
 
-    public @Test void methodGetContextURL() throws Exception
+    public @Test void methodDirectory() throws Exception
     {
         LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
-        assertEquals("http://foo.com/ctx", link.getContextURL());
+        assertEquals("http://foo.com/ctx/", link.directory().toString());
         link = newInstance("http://foo.com");
-        assertEquals("http://foo.com", link.getContextURL());
+        assertEquals("http://foo.com", link.directory().toString());
+    }
+
+    public @Test void methodRoot() throws Exception
+    {
+        LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
+        assertEquals("http://foo.com", link.root().toString());
+        link = newInstance("http://foo.com");
+        assertEquals("http://foo.com", link.root().toString());
+        link = newInstance("dev@velocity.apache.org");
+        assertNull(link.root());
+    }
+
+    public @Test void methodSetForceRelative_boolean() throws Exception
+    {
+        LinkTool link = newInstance("http://apache.org/bar");
+        assertEquals("http://apache.org/bar", link.toString());
+        link.setForceRelative(true);
+        assertEquals("/bar", link.toString());
     }
 
     public @Test void methodRelative_Object() throws Exception
     {
         LinkTool link = newInstance("/ctx/request.vm?this=that#anc");
         assertEquals("/ctx/request.vm", link.getPath());
-        assertEquals("/ctx", link.getContextPath());
+        assertEquals("/ctx/", link.getDirectory());
         assertEquals("this=that", link.getQuery());
         assertEquals("anc", link.getAnchor());
         assertEquals("/ctx/request.vm?this=that#anc", link.toString());
-        assertEquals("/ctx?this=that#anc", link.relative(null).toString());
+        assertEquals("/ctx/?this=that#anc", link.relative(null).toString());
         assertEquals("/ctx/other.vm?this=that#anc", link.relative("other.vm").toString());
         link = newInstance("http://foo.com/bar/");
-        assertEquals("http://foo.com/bar/woogie.vm", link.relative("woogie.vm").toString());
+        assertEquals("/bar/woogie.vm", link.relative("woogie.vm").toString());
         link = newInstance("/bar/");
         assertEquals("/bar/foo/woogie.vm", link.relative("foo/woogie.vm").toString());
         assertEquals("/bar/yo", link.relative("yo").toString());
     }
 
+    public @Test void methodIsRelative() throws Exception
+    {
+        LinkTool link = newInstance("/ctx/request.vm?this=that#anc");
+        assertTrue(link.isRelative());
+        link = newInstance("http://foo.com/bar.vm?q=woogie");
+        assertFalse(link.isRelative());
+        assertTrue(link.relative().isRelative());
+        link = newInstance("http://apache.org").relative("foo.vm");
+        assertTrue(link.isRelative());
+    }
+
     public @Test void methodAbsolute_Object() throws Exception
     {
         LinkTool link = newInstance();
         LinkTool result = link.absolute(null);
-        // nulls should be handled by uri()
-        assertEquals(link.uri(null), result);
 
         result = link.absolute("http://apache.org");
         assertEquals(link.uri("http://apache.org"), result);
@@ -409,7 +454,9 @@
         result = result.host("apache.org");
         assertEquals("http://apache.org/test/foo.vm", result.toString());
         result = result.absolute("bar.vm");
-        assertEquals("http://apache.org/bar.vm", result.toString());
+        assertEquals("http://apache.org/test/bar.vm", result.toString());
+        result = result.absolute("/woogie.vm");
+        assertEquals("http://apache.org/woogie.vm", result.toString());
     }
 
     public @Test void methodGetBaseRef() throws Exception
@@ -442,6 +489,20 @@
         assertEquals("velocity.apache.org", link.getHost());
     }
 
+    public @Test void methodToURI_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        URI uri = new URI("http://apache.org");
+        assertSame(uri, link.toURI(uri));
+        assertNull(link.toURI("*%&$^%#$*&^!"));
+        assertNotNull(link.toURI("http://velocity.apache.org"));
+        assertNotNull(link.toURI(new Object() {
+            public String toString() {
+                return "http://google.com";
+            }
+        }));
+    }
+
     public @Test void methodCreateURI() throws Exception
     {
         LinkTool link = newInstance();
@@ -471,6 +532,16 @@
         assertEquals("mailto:nbubna@apache.org", link.uri(uri).toString());
     }
 
+    public @Test void methodSetAppendParams_boolean() throws Exception
+    {
+        LinkTool link = newInstance("/bar?a=b");
+        LinkTool result = link.param("a","c");
+        assertEquals("a=b&amp;a=c", result.getQuery());
+        link.setAppendParams(false);
+        result = link.param("a", "d");
+        assertEquals("a=d", result.getQuery());
+    }
+
     public @Test void methodSetQuery_Object() throws Exception
     {
         LinkTool link = newInstance("/bar?a=b");
@@ -485,6 +556,15 @@
         assertEquals(null, link.getQuery());
     }
 
+    public @Test void methodNormalizeQuery_String() throws Exception
+    {
+        LinkTool link = new LinkTool();
+        assertEquals("a=b", link.normalizeQuery("a=b"));
+        assertEquals("a=b&amp;b=c", link.normalizeQuery("a=b&b=c"));
+        assertEquals("a=b&amp;b=c", link.normalizeQuery("a=b&amp;b=c"));
+        assertEquals("a=b&amp;b=c&amp;t=f", link.normalizeQuery("a=b&amp;b=c&t=f"));
+    }
+
     public @Test void methodGetQuery() throws Exception
     {
         LinkTool link = newInstance();
@@ -532,6 +612,19 @@
         assertEquals("z=3", link.getQuery());
     }
 
+    public @Test void methodToQuery_Map() throws Exception
+    {
+        LinkTool link = new LinkTool();
+        Map test = new LinkedHashMap();
+        test.put("a", "b");
+        assertEquals("a=b", link.toQuery(test));
+        test.put("b", "c");
+        assertEquals("a=b&amp;b=c", link.toQuery(test));
+        Boolean[] array = new Boolean[] { Boolean.TRUE, Boolean.FALSE };
+        test.put("b", array);
+        assertEquals("a=b&amp;b=true&amp;b=false", link.toQuery(test));
+    }
+
     public @Test void methodToQuery_ObjectObject() throws Exception
     {
         LinkTool link = newInstance();
@@ -542,16 +635,103 @@
         assertEquals("path=%2Ffoo+bar%2Fnew", link.toQuery("path", "/foo bar/new"));
     }
 
+    public @Test void methodSetParam_ObjectObjectboolean() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.query);
+        link.setParam("a","b", true);
+        assertNotNull(link.query);
+        assertEquals("a=b", link.getQuery());
+        link.setParam("a", "c", true);
+        assertEquals("a=b&amp;a=c", link.getQuery());
+        link.setParam("a", "foo", false);
+        assertEquals("a=foo", link.getQuery());
+    }
+
+    public @Test void methodSetParams_Objectboolean() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.query);
+        link.setParams("a=b", true);
+        assertNotNull(link.query);
+        assertEquals("a=b", link.getQuery());
+        link.setParams("a=c&a=d", true);
+        assertEquals("a=b&amp;a=c&amp;a=d", link.getQuery());
+        Map test = new LinkedHashMap();
+        test.put("a", "foo");
+        link.setParams(test, false);
+        assertEquals("a=foo", link.getQuery());
+    }
+
+    public @Test void methodParams_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        Map test = new LinkedHashMap();
+        test.put("a", "foo");
+        assertEquals("a=foo", link.params(test).getQuery());
+        assertEquals("a=b&amp;b=c", link.params("a=b&b=c").getQuery());
+        link = newInstance("/foo?q=a&a=q");
+        assertEquals("/foo", link.params(false).toString());
+        assertEquals("/foo?q=a&amp;a=q", link.params(true).toString());
+    }
+
     public @Test void methodParam_ObjectObject() throws Exception
     {
         LinkTool link = newInstance();
-        assertEquals("null=", link.param(null ,null).getQuery());
+        assertEquals("null=", link.param(null, null).getQuery());
         assertEquals("x=1", link.param("x",1).getQuery());
         assertEquals("x=1&amp;y=2", link.param("x",1).param("y",2).getQuery());
         link = newInstance("/hee/haa.vm?a=b");
         assertEquals("/hee/haa.vm?a=b&amp;b=true", link.param('b', true).toString());
     }
 
+    public @Test void methodAppend_ObjectObject() throws Exception
+    {
+        LinkTool link = newInstance();
+        link.setAppendParams(false); //using append(key,val) should override
+        assertEquals("x=1", link.append("x",1).getQuery());
+        assertEquals("x=1&amp;x=2", link.append("x",1).append("x",2).getQuery());
+    }
+
+    public @Test void methodSet_ObjectObject() throws Exception
+    {
+        LinkTool link = newInstance();
+        link.setAppendParams(true); //using set(key,val) should override
+        assertEquals("x=1", link.set("x",1).getQuery());
+        assertEquals("x=2", link.set("x",1).set("x",2).getQuery());
+    }
+
+    public @Test void methodRemove_Object() throws Exception
+    {
+        LinkTool link = newInstance("/foo?q=bar");
+        assertEquals("q=bar", link.getQuery());
+        assertEquals("", link.remove("q").getQuery());
+        assertEquals("q=bar", link.set("x",1).remove("x").getQuery());
+    }
+
+    public @Test void methodRemoveParam_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.removeParam("a"));
+        link.setParam("a","b",true);
+        assertEquals("a=b", link.getQuery());
+        assertEquals("b", link.removeParam("a"));
+        assertNull(link.removeParam("a"));
+    }
+
+    public @Test void methodHandleParamsBoolean_boolean() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.query);
+        link.setParam("a","b",true);
+        Map q = link.query;
+        link.handleParamsBoolean(true);
+        assertSame(q, link.query);
+        assertEquals("a=b", link.getQuery());
+        link.handleParamsBoolean(false);
+        assertNull(link.query);
+    }
+
     public @Test void methodParams_Map() throws Exception
     {
         LinkTool link = newInstance("http://go.com");
@@ -567,12 +747,14 @@
     public @Test void methodParseQuery_String() throws Exception
     {
         LinkTool link = newInstance();
-        Map result = link.parseQuery("a=b&amp;x=1");
+        Map result = link.parseQuery("a=b&x=1");
         assertEquals("b", result.get("a"));
         assertEquals("1", result.get("x"));
         link.setXHTML(false);
-        result = link.parseQuery("true=false");
+        result = link.parseQuery("true=false&amp;false=true&black=white");
         assertEquals("false", result.get("true"));
+        assertEquals("true", result.get("false"));
+        assertEquals("white", result.get("black"));
     }
 
     public @Test void methodGetParams() throws Exception
@@ -581,9 +763,10 @@
         Map result = link.getParams();
         assertEquals("b", result.get("a"));
         assertEquals("true", result.get("x"));
-        result = link.param('y',false).getParams();
-        assertEquals("b", result.get("a"));
-        assertEquals("false", result.get("y"));
+        Map newresult = link.param('y',false).getParams();
+        assertFalse(result.equals(newresult));
+        assertEquals("b", newresult.get("a"));
+        assertEquals(Boolean.FALSE, newresult.get("y"));
     }
 
     public @Test void methodSetFragment_Object() throws Exception
@@ -595,7 +778,7 @@
         assertEquals(null, link.toString());
         link = newInstance("/foo#bar");
         link.setFragment("woo gie");
-        assertEquals("/foo#woo%20gie", link.toString());
+        assertEquals("/foo#woo+gie", link.toString());
     }
 
     public @Test void methodGetAnchor() throws Exception
@@ -624,7 +807,7 @@
         assertEquals("a b", link.anchor(space).getAnchor());
         link = newInstance("http://go.com#foo");
         assertEquals("http://go.com#true", link.anchor(true).toString());
-        assertEquals("http://go.com#a%20b", link.anchor(space).toString());
+        assertEquals("http://go.com#a+b", link.anchor(space).toString());
     }
 
     public @Test void methodGetSelf() throws Exception