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/06/05 21:36:06 UTC

svn commit: r663715 - in /velocity/tools/trunk: src/main/java/org/apache/velocity/tools/generic/ src/main/java/org/apache/velocity/tools/view/ src/test/java/org/apache/velocity/tools/ xdocs/

Author: nbubna
Date: Thu Jun  5 12:36:05 2008
New Revision: 663715

URL: http://svn.apache.org/viewvc?rev=663715&view=rev
Log:
add generic version of LinkTool

Added:
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java   (with props)
    velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java   (with props)
Modified:
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/LinkTool.java
    velocity/tools/trunk/xdocs/changes.xml
    velocity/tools/trunk/xdocs/dependencies.xml
    velocity/tools/trunk/xdocs/generic.project.xml
    velocity/tools/trunk/xdocs/generic.xml

Added: 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=663715&view=auto
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java (added)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java Thu Jun  5 12:36:05 2008
@@ -0,0 +1,1125 @@
+package org.apache.velocity.tools.generic;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.velocity.runtime.log.Log;
+import org.apache.velocity.tools.Scope;
+import org.apache.velocity.tools.ToolContext;
+import org.apache.velocity.tools.config.DefaultKey;
+import org.apache.velocity.tools.config.ValidScope;
+
+/**
+ * <p>The LinkTool provides many methods to work with URIs and can help you:
+ * <ul>
+ *     <li>construct full URIs (opaque, absolute or relative)</li>
+ *     <li>encode and decode URLs (part or whole)</li>
+ *     <li>retrieve path info for the current request</li>
+ *     <li>and more..</li>
+ * </ul></p>
+ *
+ * <p>This GenericTools (i.e. non-servlet based) version of LinkTool
+ * is largely based upon the same API and behavior as the older
+ * VelocityView version, with a few differences, particularly in
+ * internal representation and query handling.  You can expect that
+ * in the future work will be done to more closely align the APIs.
+ * It is likely that the VelocityView version will become a subclass
+ * of this version that adds on servlet-awareness and related features.
+ * For now, though, they are entirely separate but similar tools.
+ * </p>
+ *
+ * <p>The LinkTool is somewhat special in that nearly all public methods return
+ * a new instance of LinkTool. This facilitates greatly the repeated use
+ * of the LinkTool in Velocity and leads to an elegant syntax.</p>
+ * 
+ * <p><pre>
+ * Template example(s):
+ *   #set( $base = $link.relative('MyPage.vm').anchor('view') )
+ *   &lt;a href="$base.param('select','this')"&gt;this&lt;/a&gt;
+ *   &lt;a href="$base.param('select','that')"&gt;that&lt;/a&gt;
+ *
+ * Toolbox configuration:
+ * &lt;tools&gt;
+ *   &lt;toolbox scope="request"&gt;
+ *     &lt;tool class="org.apache.velocity.tools.generic.LinkTool"
+ *              uri="http://velocity.apache.org/tools/devel/"/&gt;
+ *   &lt;/toolbox&gt;
+ * &lt;/tools&gt;
+ * </pre></p>
+ *
+ * @author Nathan Bubna
+ * @since VelocityTools 2.0
+ * @version $Id: LinkTool.java 601976 2007-12-07 03:50:51Z nbubna $
+ */
+@DefaultKey("link")
+@ValidScope(Scope.REQUEST)
+public class LinkTool extends AbstractLockConfig implements Cloneable
+{
+    /** Standard HTML delimiter for query data ('&') */
+    public static final String HTML_QUERY_DELIMITER = "&";
+
+    /** XHTML delimiter for query data ('&amp;amp;') */
+    public static final String XHTML_QUERY_DELIMITER = "&amp;";
+
+    public static final String DEFAULT_CHARSET = "UTF-8";
+    public static final String DEFAULT_SCHEME = "http";
+    public static final String SECURE_SCHEME = "https";
+
+    public static final String URI_KEY = "uri";
+    public static final String SCHEME_KEY = "scheme";
+    public static final String USER_KEY = "user";
+    public static final String HOST_KEY = "host";
+    public static final String PORT_KEY = "port";
+    public static final String PATH_KEY = ToolContext.PATH_KEY;
+    public static final String QUERY_KEY = "params";
+    public static final String FRAGMENT_KEY = "anchor";
+    public static final String CHARSET_KEY = "charset";
+    public static final String XHTML_MODE_KEY = "xhtml";
+
+    protected Log LOG;
+    protected String scheme;
+    protected String user;
+    protected String host;
+    protected int port;
+    protected String path;
+    protected String query;
+    protected String fragment;
+    protected String charset;
+    protected String queryDelim;
+
+    private boolean opaque;
+    private final LinkTool self;
+
+
+    /**
+     * Default constructor. Tool must be initialized before use.
+     */
+    public LinkTool()
+    {
+        scheme = null;
+        user = null;
+        host = null;
+        port = -1;
+        path = null;
+        query = null;
+        fragment = null;
+        charset = DEFAULT_CHARSET;
+        queryDelim = XHTML_QUERY_DELIMITER;
+        opaque = false;
+        self = this;
+    }
+
+    private void logError(String msg, Throwable t)
+    {
+        if (this.LOG != null)
+        {
+            this.LOG.error("LinkTool: "+msg, t);
+        }
+    }
+
+
+    // --------------------------------------- Setup Methods -------------
+
+    protected void configure(ValueParser props)
+    {
+        this.LOG = (Log)props.getValue(ToolContext.LOG_KEY);
+
+        String link = props.getString(URI_KEY);
+        if (link != null)
+        {
+            setFromURI(link);
+        }
+
+        String schm = props.getString(SCHEME_KEY);
+        if (schm != null)
+        {
+            setScheme(schm);
+        }
+        String info = props.getString(USER_KEY);
+        if (info != null)
+        {
+            setUserInfo(info);
+        }
+        String hst = props.getString(HOST_KEY);
+        if (hst != null)
+        {
+            setHost(hst);
+        }
+        Integer prt = props.getInteger(PORT_KEY);
+        if (prt != null)
+        {
+            setPort(prt.intValue());
+        }
+        String pth = props.getString(PATH_KEY);
+        if (pth != null)
+        {
+            setPath(pth);
+        }
+        String params = props.getString(QUERY_KEY);
+        if (params != null)
+        {
+            setQuery(params);
+        }
+        String anchor = props.getString(FRAGMENT_KEY);
+        if (anchor != null)
+        {
+            setFragment(anchor);
+        }
+
+        String chrst = props.getString(CHARSET_KEY);
+        if (chrst != null)
+        {
+            this.charset = chrst;
+        }
+
+        Boolean xhtml = props.getBoolean(XHTML_MODE_KEY);
+        if (xhtml != null)
+        {
+            setXHTML(xhtml);
+        }
+    }
+
+    /**
+     * <p>Controls the delimiter used for separating query data pairs.
+     *    By default, the standard '&' character is used.</p>
+     * <p>This is not exposed to templates as this decision is best not
+     *    made at that level.</p>
+     * <p>Subclasses may easily override the init() method to set this
+     *    appropriately and then call super.init()</p>
+     *
+     * @param useXhtml if true, the XHTML query data delimiter ('&amp;amp;')
+     *        will be used.  if false, then '&' will be used.
+     * @see <a href="http://www.w3.org/TR/xhtml1/#C_12">Using Ampersands in Attribute Values (and Elsewhere)</a>
+     */
+    protected void setXHTML(boolean xhtml)
+    {
+        queryDelim = (xhtml) ? XHTML_QUERY_DELIMITER : HTML_QUERY_DELIMITER;
+    }
+
+    /**
+     * Equivalent to clone, but with no checked exceptions.
+     * If for some unfathomable reason clone() doesn't work,
+     * this will throw a RuntimeException.
+     */
+    protected LinkTool duplicate()
+    {
+        try
+        {
+            return (LinkTool)this.clone();
+        }
+        catch (CloneNotSupportedException e)
+        {
+            String msg = "Could not properly clone " + getClass();
+            logError(msg, e);
+            throw new RuntimeException(msg, e);
+        }
+    }
+
+    protected void setScheme(Object obj)
+    {
+        if (obj == null)
+        {
+            this.scheme = null;
+        }
+        else
+        {
+            this.scheme = String.valueOf(obj);
+            if (scheme.endsWith(":"))
+            {
+                scheme = scheme.substring(0, scheme.length() - 1);
+            }
+        }
+    }
+
+    protected void setUserInfo(Object obj)
+    {
+        this.user = obj == null ? null : String.valueOf(obj);
+    }
+
+    protected void setHost(Object obj)
+    {
+        this.host = obj == null ? null : String.valueOf(obj);
+    }
+
+    protected void setPort(Object obj)
+    {
+        if (obj == null)
+        {
+            this.port = -1;
+        }
+        else if (obj instanceof Number)
+        {
+            this.port = ((Number)obj).intValue();
+        }
+        else
+        {
+            try
+            {
+                this.port = Integer.parseInt(String.valueOf(obj));
+            }
+            catch (NumberFormatException nfe)
+            {
+                logError("Could convert '"+obj+"' to int", nfe);
+                this.port = -2; // use this to mean error
+            }
+        }
+    }
+
+    protected void setPath(Object obj)
+    {
+        if (obj == null)
+        {
+            this.path = null;
+        }
+        else
+        {
+            this.path = String.valueOf(obj);
+            if (!this.opaque && !path.startsWith("/"))
+            {
+                this.path = '/' + this.path;
+            }
+        }
+    }
+
+    protected void appendPath(Object obj)
+    {
+        if (obj != null && !this.opaque)
+        {
+            setPath(combinePath(getPath(), String.valueOf(obj)));
+        }
+    }
+
+    protected String combinePath(String start, String end)
+    {
+        if (end == null)
+        {
+            return start;
+        }
+        if (start == null)
+        {
+            return end;
+        }
+
+        // make sure we don't get // or nothing between start and end
+        boolean startEnds = start.endsWith("/");
+        boolean endStarts = end.startsWith("/");
+        if (startEnds ^ endStarts) //one
+        {
+            return start + end;
+        }
+        else if (startEnds & endStarts) //both
+        {
+            return start + end.substring(1, end.length());
+        }
+        else //neither
+        {
+            return start + '/' + end;
+        }
+    }
+
+    protected void setQuery(Object obj)
+    {
+        if (obj == null)
+        {
+            this.query = null;
+        }
+        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);
+            }
+        }
+    }
+
+    protected void appendQuery(Object obj)
+    {
+        if (obj != null)
+        {
+            setQuery(combineQuery(getQuery(), String.valueOf(obj)));
+        }
+    }
+
+    protected String combineQuery(String current, String add)
+    {
+        if (add == null || add.length() == 0)
+        {
+            return current;
+        }
+        if (add.startsWith("?"))
+        {
+            add = add.substring(1, add.length());
+        }
+        if (current == null)
+        {
+            return add;
+        }
+        if (current.endsWith(queryDelim))
+        {
+            current = current.substring(0, current.length() - queryDelim.length());
+        }
+        else if (current.endsWith("&"))
+        {
+            current = current.substring(0, current.length() - 1);
+        }
+        if (add.startsWith(queryDelim))
+        {
+            return current + add;
+        }
+        else if (add.startsWith("&"))
+        {
+            // drop the html delim in favor of the xhtml one
+            add = add.substring(1, add.length());
+        }
+        return current + queryDelim + add;
+    }
+
+    protected String toQuery(Object key, Object value)
+    {
+        StringBuilder out = new StringBuilder();
+        if (value == null)
+        {
+            out.append(encode(key));
+            out.append('=');
+            /* Interpret null as "no value" */
+        }
+        else if (value instanceof List)
+        {
+            appendAsArray(out, key, ((List)value).toArray());
+        }
+        else if (value instanceof Object[])
+        {
+            appendAsArray(out, key, (Object[])value);
+        }
+        else
+        {
+            out.append(encode(key));
+            out.append('=');
+            out.append(encode(value));
+        }
+        return out.toString();
+    }
+
+    /* Utility method to avoid logic duplication in toQuery() */
+    private void appendAsArray(StringBuilder out, Object key, Object[] arr)
+    {
+        String encKey = encode(key);
+        for (int i=0; i < arr.length; i++)
+        {
+            out.append(encKey);
+            out.append('=');
+            if (arr[i] != null)
+            {
+                out.append(encode(arr[i]));
+            }
+            if (i+1 < arr.length)
+            {
+                out.append(queryDelim);
+            }
+        }
+    }
+
+    protected Map<String,Object> parseQuery(String query)
+    {
+        if (query.startsWith("?"))
+        {
+            query = query.substring(1, query.length());
+        }
+        String[] pairs = query.split(queryDelim);
+        if (pairs.length == 0)
+        {
+            return null;
+        }
+        Map<String,Object> params = new LinkedHashMap<String,Object>(pairs.length);
+        for (String pair : pairs)
+        {
+            String[] kv = pair.split("=");
+            String key = kv[0];
+            Object value = kv.length > 1 ? kv[1] : null;
+            if (params.containsKey(kv[0]))
+            {
+                Object oldval = params.get(key);
+                if (oldval instanceof List)
+                {
+                    ((List)oldval).add((String)value);
+                    value = oldval;
+                }
+                else
+                {
+                    List<String> list = new ArrayList<String>();
+                    list.add((String)oldval);
+                    list.add((String)value);
+                    value = list;
+                }
+            }
+            params.put(key, value);
+        }
+        return params;
+    }
+
+    protected void setFragment(Object obj)
+    {
+        if (obj == null)
+        {
+            this.fragment = null;
+        }
+        else
+        {
+            this.fragment = String.valueOf(obj);
+            if (this.fragment.length() == 0)
+            {
+                this.fragment = null;
+            }
+        }
+    }
+
+    protected boolean setFromURI(Object obj)
+    {
+        if (obj == null)
+        {
+            // clear everything out...
+            setScheme(null);
+            setUserInfo(null);
+            setHost(null);
+            setPort(null);
+            setPath(null);
+            setQuery(null);
+            setFragment(null);
+            return true;
+        }
+
+        URI uri;
+        if (obj instanceof URI)
+        {
+            uri = (URI)obj;
+        }
+        else
+        {
+            try
+            {
+                uri = new URI(String.valueOf(obj));
+            }
+            catch (Exception e)
+            {
+                logError("Could convert '"+obj+"' to URI", e);
+                return false;
+            }
+        }
+        setScheme(uri.getScheme());
+        if (uri.isOpaque())
+        {
+            this.opaque = true;
+            // path is used as scheme-specific part
+            setPath(uri.getSchemeSpecificPart());
+        }
+        else
+        {
+            setUserInfo(uri.getUserInfo());
+            setHost(uri.getHost());
+            setPort(uri.getPort());
+            String pth = uri.getPath();
+            if (pth.equals("/") || pth.length() == 0)
+            {
+                pth = null;
+            }
+            setPath(pth);
+            setQuery(uri.getQuery());
+        }
+        setFragment(uri.getFragment());
+        return true;
+    }
+
+    protected URI createURI()
+    {
+        try
+        {
+            // fail if there was an error in setting the port
+            if (port > -2)
+            {
+                String anchor = this.fragment;
+                if (anchor != null)
+                {
+                    anchor = encode(anchor);
+                }
+                if (opaque)
+                {
+                    // path is used as scheme-specific part
+                    return new URI(scheme, path, anchor);
+                }
+                else
+                {
+                    // only create the URI if we have some values besides a port
+                    if (scheme == null && user == null && host == null
+                        && path == null && query == null && fragment == null)
+                    {
+                        return null;
+                    }
+                    return new URI(scheme, user, host, port, path, query, fragment);
+                }
+            }
+        }
+        catch (Exception e)
+        {
+            logError("Could not create URI", e);
+        }
+        return null;
+    }
+
+    // --------------------------------------------- Template Methods -----------
+
+    public LinkTool scheme(Object scheme)
+    {
+        LinkTool copy = duplicate();
+        copy.setScheme(scheme);
+        return copy;
+    }
+
+    public LinkTool secure()
+    {
+        return scheme(SECURE_SCHEME);
+    }
+
+    public LinkTool insecure()
+    {
+        return scheme(DEFAULT_SCHEME);
+    }
+
+    public String getScheme()
+    {
+        return scheme;
+    }
+
+    public boolean isSecure()
+    {
+        return SECURE_SCHEME.equalsIgnoreCase(getScheme());
+    }
+
+    public boolean isAbsolute()
+    {
+        if (this.scheme == null)
+        {
+            return false;
+        }
+        return true;
+    }
+
+    public boolean isOpaque()
+    {
+        return this.opaque;
+    }
+
+    public LinkTool user(Object info)
+    {
+        LinkTool copy = duplicate();
+        copy.setUserInfo(info);
+        return copy;
+    }
+
+    public String getUser()
+    {
+        return this.user;
+    }
+
+    public LinkTool host(Object host)
+    {
+        LinkTool copy = duplicate();
+        copy.setHost(host);
+        // if we have host but no scheme
+        if (copy.getHost() != null && !copy.isAbsolute())
+        {
+            // use default scheme
+            copy.setScheme(DEFAULT_SCHEME);
+        }
+        return copy;
+    }
+
+    public String getHost()
+    {
+        return this.host;
+    }
+
+    public LinkTool port(Object port)
+    {
+        LinkTool copy = duplicate();
+        copy.setPort(port);
+        return copy;
+    }
+
+    public Integer getPort()
+    {
+        if (this.port < 0)
+        {
+            return null;
+        }
+        return this.port;
+    }
+
+    public LinkTool path(Object pth)
+    {
+        LinkTool copy = duplicate();
+        copy.setPath(pth);
+        return copy;
+    }
+
+    public String getPath()
+    {
+        return this.path;
+    }
+
+    public LinkTool append(Object pth)
+    {
+        LinkTool copy = duplicate();
+        copy.appendPath(pth);
+        return copy;
+    }
+
+    /**
+     * At this level, this method returns all "folders"
+     * in the set {@link #getPath()} value, by just trimming
+     * of the last "/" and all that follows.
+     */
+    public String getContextPath()
+    {
+        if (this.path == null || this.opaque)
+        {
+            return null;
+        }
+        int lastSlash = this.path.lastIndexOf('/');
+        if (lastSlash <= 0)
+        {
+            return "";
+        }
+        return this.path.substring(0, lastSlash);
+    }
+
+    /**
+     * At this level, this method returns the last section
+     * of the path, from the final "/" onward.
+     */
+    public String getRequestPath()
+    {
+        if (this.path == null || this.opaque)
+        {
+            return null;
+        }
+        int lastSlash = this.path.lastIndexOf('/');
+        if (lastSlash <= 0)
+        {
+            return this.path;
+        }
+        return this.path.substring(lastSlash, this.path.length());
+    }
+
+    /**
+     * Returns the "root" for this URI, if it has one.
+     * This does not stick close to URI dogma and will
+     * try to insert the default scheme if there is none,
+     * and will return null if there is no host. It
+     * is also unfriendly to opaque URIs.
+     */
+    public String getRoot()
+    {
+        if (host == 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)))
+        {
+            out.append(':');
+            out.append(port);
+        }
+        return out.toString();
+    }
+        
+
+    /**
+     * <p>Returns the URI that addresses this web application. E.g.
+     * <code>http://myserver.net/myapp</code>. This string does not end
+     * with a "/".  Note! This will not represent any URI reference or
+     * query data set for this LinkTool.</p>
+     */
+    public String getContextURL()
+    {
+        String root = getRoot();
+        if (root == null)
+        {
+            return null;
+        }
+        return combinePath(root, getContextPath());
+    }
+
+    /**
+     * <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>
+     *
+     * Example:<br>
+     * <code>&lt;a href='$link.relative("/templates/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>
+     *
+     * @param path 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
+     */
+    public LinkTool relative(Object obj)
+    {
+        if (obj == null)
+        {
+            return path(getContextPath());
+        }
+        String pth = String.valueOf(obj);
+        LinkTool copy = duplicate();
+        // prepend relative paths with context path
+        copy.setPath(combinePath(getContextPath(), pth));
+        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>
+     *
+     * Example:<br>
+     * <code>&lt;a href='$link.absolute("/templates/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>
+     * 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 uri 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
+     */
+    public LinkTool absolute(Object obj)
+    {
+        if (obj == null)
+        {
+            // use uri's null handling
+            return uri(obj);
+        }
+        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)
+        {
+            return uri(combinePath(root, 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;
+    }
+
+    /**
+     * <p>Returns a copy of the link with the given URI reference set.
+     * No conversions are applied to the given URI reference. The URI
+     * reference can be absolute, server-relative, relative and may
+     * contain query parameters. This method will overwrite any
+     * previous URI reference settings but will copy the query
+     * string.</p>
+     *
+     * @param uri URI reference to set
+     * @return a new instance of LinkTool
+     * @since VelocityTools 1.3
+     */
+    public LinkTool uri(Object uri)
+    {
+        LinkTool copy = duplicate();
+        if (copy.setFromURI(uri))
+        {
+            return copy;
+        }
+        return null;
+    }
+
+    public URI getUri()
+    {
+        if (!isSafeMode())
+        {
+            return createURI();
+        }
+        return null;
+    }
+
+    /**
+     * Returns the full URI of this template without any query data.
+     * e.g. <code>http://myserver.net/myapp/stuff/View.vm</code>
+     * Note! The returned String will not represent any URI reference
+     * or query data set for this LinkTool. A typical application of
+     * this method is with the HTML base tag. For example:
+     * <code>&lt;base href="$link.baseRef"&gt;</code>
+     */
+    public String getBaseRef()
+    {
+        LinkTool copy = duplicate();
+        copy.setQuery(null);
+        copy.setFragment(null);
+        return copy.toString();
+    }
+
+    public LinkTool query(Object query)
+    {
+        LinkTool copy = duplicate();
+        copy.setQuery(query);
+        return copy;
+    }
+
+    public String getQuery()
+    {
+        return 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>
+     *
+     * @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));
+        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>
+     *
+     * @param parameters map of new query data keys to values
+     * @return a new instance of LinkTool
+     * @since VelocityTools 1.3
+     */
+    public LinkTool params(Map parameters)
+    {
+        // don't waste time with null/empty data
+        if (parameters == null || parameters.isEmpty())
+        {
+            return this;
+        }
+        LinkTool copy = duplicate();
+        StringBuilder query = new StringBuilder();
+        for (Object e : parameters.entrySet())
+        {
+            Map.Entry entry = (Map.Entry)e;
+            //add new pair to this LinkTool's query data
+            if (query.length() > 0)
+            {
+                query.append(queryDelim);
+            }
+            query.append(toQuery(entry.getKey(), entry.getValue()));
+        }
+        copy.appendQuery(query);
+        return copy;
+    }
+
+    public Map getParams()
+    {
+        if (this.query == null || this.query.isEmpty())
+        {
+            return null;
+        }
+        return parseQuery(this.query);
+    }
+
+    /**
+     * <p>Returns a copy of the link with the specified anchor to be
+     *    added to the end of the generated hyperlink.</p>
+     *
+     * Example:<br>
+     * <code>&lt;a href='$link.setAnchor("foo")'&gt;Foo&lt;/a&gt;</code><br>
+     * produces something like</br>
+     * <code>&lt;a href="#foo"&gt;Foo&lt;/a&gt;</code><br>
+     *
+     * @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)
+    {
+        LinkTool copy = duplicate();
+        copy.setFragment(anchor);
+        return copy;
+    }
+
+    /**
+     * Returns the anchor (internal document reference) set for this link.
+     */
+    public String getAnchor()
+    {
+        return this.fragment;
+    }
+
+    public LinkTool getSelf()
+    {
+        // there are no self-params to bother with at this level,
+        return self;
+    }
+
+    /**
+     * Returns the full URI reference that's been built with this tool,
+     * including the query string and anchor, e.g.
+     * <code>http://myserver.net/myapp/stuff/View.vm?id=42&type=blue#foo</code>.
+     * Typically, it is not necessary to call this method explicitely.
+     * Velocity will call the toString() method automatically to obtain
+     * a representable version of an object.
+     */
+    public String toString()
+    {
+        URI uri = createURI();
+        if (uri == null)
+        {
+            return null;
+        }
+        return uri.toString();
+    }
+
+    /**
+     * This instance is considered equal to any
+     * LinkTool instance whose toString() method returns a
+     * String equal to that returned by this instance's toString()
+     * @see #toString()
+     */
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (obj == null || !(obj instanceof LinkTool))
+        {
+            return false;
+        }
+        // string value is all that ultimately matters
+        String that = obj.toString();
+        if (that == null && toString() == null)
+        {
+            return true;
+        }
+        return that.equals(toString());
+    }
+
+    /**
+     * Returns the hash code for the result of toString().
+     * If toString() returns {@code null} (yes, we do break that contract),
+     * this will return {@code -1}.
+     */
+    @Override
+    public int hashCode()
+    {
+        String hashme = toString();
+        if (hashme == null)
+        {
+            return -1;
+        }
+        return hashme.hashCode();
+    }
+
+
+    /**
+     * Delegates encoding of the specified url content to
+     * {@link URLEncoder#encode} using the configured character encoding.
+     *
+     * @return String - the encoded url.
+     */
+    public String encode(Object obj)
+    {
+        if (obj == null)
+        {
+            return null;
+        }
+        try
+        {
+            return URLEncoder.encode(String.valueOf(obj), charset);
+        }
+        catch (UnsupportedEncodingException uee)
+        {
+            logError("Character encoding '"+charset+"' is unsupported", uee);
+            return null;
+        }
+    }
+
+    /**
+     * Delegates decoding of the specified url content to
+     * {@link URLDecoder#decode} using the configured character encoding.
+     *
+     * @return String - the decoded url.
+     */
+    public String decode(Object obj)
+    {
+        if (obj == null)
+        {
+            return null;
+        }
+        try
+        {
+            return URLDecoder.decode(String.valueOf(obj), charset);
+        }
+        catch (UnsupportedEncodingException uee)
+        {
+            logError("Character encoding '"+charset+"' is unsupported", uee);
+            return null;
+        }
+    }
+
+}

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java
------------------------------------------------------------------------------
    svn:keywords = Revision

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LinkTool.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml?rev=663715&r1=663714&r2=663715&view=diff
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml (original)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml Thu Jun  5 12:36:05 2008
@@ -38,6 +38,7 @@
     </toolbox>
     <toolbox scope="request">
         <tool class="org.apache.velocity.tools.generic.ContextTool"/>
+        <tool class="org.apache.velocity.tools.generic.LinkTool"/>
         <tool class="org.apache.velocity.tools.generic.LoopTool"/>
         <tool class="org.apache.velocity.tools.generic.RenderTool"/>
         <!-- 

Modified: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/LinkTool.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/LinkTool.java?rev=663715&r1=663714&r2=663715&view=diff
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/LinkTool.java (original)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/LinkTool.java Thu Jun  5 12:36:05 2008
@@ -45,6 +45,12 @@
  *     <li>and more..</li>
  * </ul></p>
  * 
+ * <p>This VelocityView version of LinkTool is not currently a subclass of the
+ * newer, GenericTools' version.  This is likely, however, to happen in the
+ * future.  To best future proof your use of this tool against deprecations,
+ * try not to rely upon differences between the two versions, except of course
+ * where this tool provides additional servlet-related functions.</p>
+ * 
  * <p>The LinkTool is somewhat special in that nearly all public methods return
  * a new instance of LinkTool. This facilitates greatly the repeated use
  * of the LinkTool in Velocity and leads to an elegant syntax.</p>

Added: 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=663715&view=auto
==============================================================================
--- velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java (added)
+++ velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java Thu Jun  5 12:36:05 2008
@@ -0,0 +1,651 @@
+package org.apache.velocity.tools.generic;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.*;
+import static org.junit.Assert.*;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.velocity.runtime.log.Log;
+import org.apache.velocity.tools.generic.ValueParser;
+
+/**
+ * <p>Tests for generic version of LinkTool</p>
+ *
+ * @author Nathan Bubna
+ * @since VelocityTools 2.0
+ * @version $Id$
+ */
+public class LinkToolTests {
+
+    public static final Map DEFAULT_PROPS = new HashMap();
+    static
+    {
+        // don't lock configure() for testing
+        DEFAULT_PROPS.put(LinkTool.SAFE_MODE_KEY, false);
+        DEFAULT_PROPS.put(LinkTool.LOCK_CONFIG_KEY, false);
+    }
+
+    /**
+     * Returns a new instance configured with the 
+     * default testing properties.
+     */
+    public LinkTool newInstance()
+    {
+        LinkTool link = new LinkTool();
+        link.configure(DEFAULT_PROPS);
+        return link;
+    }
+
+    public LinkTool newInstance(String uri)
+    {
+        return newInstance(LinkTool.URI_KEY, uri);
+    }
+
+    /**
+     * Returns a new instance configured with the 
+     * default testing properties and the specified
+     * non-default property.
+     */
+    public LinkTool newInstance(String key, Object value)
+    {
+        LinkTool link = new LinkTool();
+        Map props = new HashMap(DEFAULT_PROPS);
+        props.put(key, value);
+        link.configure(props);
+        return link;
+    }
+
+    public @Test void ctorLinkTool() throws Exception
+    {
+        try
+        {
+            new LinkTool();
+        }
+        catch (Exception e)
+        {
+            fail("Constructor 'LinkTool()' failed due to: " + e);
+        }
+    }
+
+    public @Test void methodConfigure_ValueParser() throws Exception
+    {
+        LinkTool link = newInstance("mailto:nbubna@apache.org");
+        assertEquals("mailto", link.getScheme());
+        assertEquals("mailto:nbubna@apache.org", link.toString());
+        assertTrue(link.getUri().isOpaque());
+        assertTrue(link.isAbsolute());
+    }
+
+    public @Test void methodDuplicate() throws Exception
+    {
+        LinkTool link = newInstance("http://apache.org/foo.html");
+        LinkTool result = link.duplicate();
+        assertFalse(link == result);
+        assertSame(link.getScheme(), result.getScheme());
+        assertSame(link.getUser(), result.getUser());
+        assertSame(link.getHost(), result.getHost());
+        assertSame(link.getPath(), result.getPath());
+        assertSame(link.getQuery(), result.getQuery());
+        assertSame(link.getAnchor(), result.getAnchor());
+        assertSame(link.getSelf(), result.getSelf());
+    }
+
+    public @Test void methodEncode_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals("this+has+spaces", link.encode("this has spaces"));
+        assertEquals("%40%2F+", link.encode("@/ "));
+    }
+
+    public @Test void methodDecode_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals("this has spaces", link.decode("this+has+spaces"));
+        assertEquals("@/ ", link.decode("%40%2F+"));
+    }
+
+    public @Test void methodSetScheme_Object() throws Exception
+    {
+        LinkTool link = newInstance(LinkTool.SCHEME_KEY, LinkTool.DEFAULT_SCHEME);
+        assertEquals(LinkTool.DEFAULT_SCHEME, link.getScheme());
+        link.setScheme(null);
+        assertEquals(null, link.getScheme());
+        link.setScheme("foo:");
+        assertEquals("foo", link.getScheme());
+    }
+
+    public @Test void methodScheme_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        LinkTool result = link.scheme(null);
+        assertEquals(null, result.getScheme());
+        link = newInstance("https://apache.org");
+        assertEquals(null, link.getPath());
+        assertEquals("https://apache.org", link.toString());
+        assertEquals(LinkTool.SECURE_SCHEME, link.getScheme());
+        assertTrue(link.isSecure());
+        result = link.scheme(LinkTool.DEFAULT_SCHEME);
+        assertEquals("http://apache.org", result.toString());
+        assertFalse(result.isSecure());
+    }
+
+    public @Test void methodGetScheme() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals(null, link.getScheme());
+        assertEquals("mailto", link.scheme("mailto").getScheme());
+    }
+
+    public @Test void methodSecure() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertFalse(link.isSecure());
+        LinkTool result = link.secure();
+        assertTrue(result.isSecure());
+    }
+
+    public @Test void methodInsecure() throws Exception
+    {
+        LinkTool link = newInstance("https://apache.org");
+        assertTrue(link.isSecure());
+        LinkTool result = link.insecure();
+        assertFalse(result.isSecure());
+    }
+
+    public @Test void methodIsAbsolute() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertFalse(link.isAbsolute());
+        LinkTool result = link.absolute("http://apache.org");
+        assertTrue(result.isAbsolute());
+    }
+
+    public @Test void methodSetUserInfo_Object() throws Exception
+    {
+        LinkTool link = newInstance(LinkTool.USER_KEY, "nbubna");
+        assertEquals("nbubna", link.getUser());
+        link.setUserInfo(null);
+        assertEquals(null, link.getUser());
+        // no encoding should happen here
+        link.setUserInfo("@#$ /@!");
+        assertEquals("@#$ /@!", link.getUser());
+    }
+
+    public @Test void methodGetUser() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals(null, link.getUser());
+        link = newInstance("http://nbubna@apache.org");
+        assertEquals("nbubna", link.getUser());
+    }
+
+    public @Test void methodUser_Object() throws Exception
+    {
+        LinkTool link = newInstance("http://nbubna@apache.org");
+        assertEquals(null, link.user(null).getUser());
+        assertEquals("nbubna", link.user("nbubna").getUser());
+        assertEquals("@#$ /!", link.user("@#$ /!").getUser());
+        assertEquals("http://%40%23$%20%2F!@apache.org", link.user("@#$ /!").toString());
+    }
+
+    public @Test void methodGetHost() throws Exception
+    {
+        LinkTool link = newInstance("http://apache.org");
+        assertEquals("apache.org", link.getHost());
+        link.setFromURI("http://velocity.apache.org/tools/devel/");
+        assertEquals("velocity.apache.org", link.getHost());
+    }
+
+    public @Test void methodHost_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals("apache.org", link.host("apache.org").getHost());
+        link = newInstance("https://nbubna@www.apache.org");
+        assertEquals("https://nbubna@people.apache.org", link.host("people.apache.org").toString());
+    }
+
+    public @Test void methodSetHost_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        link.setHost("foo.com");
+        assertEquals("foo.com", link.getHost());
+    }
+
+    public @Test void methodGetPort() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.getPort());
+        link = newInstance(LinkTool.PORT_KEY, 42);
+        assertEquals(42, link.getPort());
+    }
+
+    public @Test void methodPort_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.port(null).getPort());
+        assertNull(link.port(":asd").getPort());
+        assertEquals(1, link.port(1).getPort());
+        assertEquals(42, link.port("42").getPort());
+    }
+
+    public @Test void methodSetPort_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        link.setPort(42);
+        assertEquals(42, link.getPort());
+    }
+
+    public @Test void methodGetPath() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.getPath());
+        link = newInstance("http://velocity.apache.org/tools/devel");
+        assertEquals("/tools/devel", link.getPath());
+    }
+
+    public @Test void methodSetPath_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.getPath());
+        link.setPath("foo");
+        assertEquals("/foo", link.getPath());
+        link.setPath("/foo");
+        assertEquals("/foo", link.getPath());
+        link.setPath("/foo/");
+        assertEquals("/foo/", link.getPath());
+        link.setPath("/foo/");
+        assertEquals("/foo/", link.getPath());
+    }
+
+    public @Test void methodPath_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.getPath());
+        assertEquals("/bar", link.path("bar").getPath());
+        assertEquals("/bar", link.path("/bar").getPath());
+        assertEquals("/bar/", link.path("bar/").getPath());
+        assertEquals("/bar/", link.path("/bar/").getPath());
+        link = newInstance("http://foo.com/this/that.vm");
+        assertEquals("http://foo.com/this/that.vm", link.toString());
+        assertEquals("http://foo.com/bar.vm", link.path("bar.vm").toString());
+    }
+
+    public @Test void methodCombinePath_StringString() throws Exception
+    {
+        LinkTool link = newInstance();
+        String none = null;
+        String empty = "";
+        String test = "test";
+        String starts = "/this";
+        String ends = "that/";
+        String both = "/these/";
+        assertNull(link.combinePath(none, none));
+        assertSame(empty, link.combinePath(none, empty));
+        assertSame(empty, link.combinePath(empty, none));
+        assertEquals("/test", link.combinePath(empty, test));
+        assertEquals("test/", link.combinePath(test, empty));
+        assertEquals("/this/this", link.combinePath(starts, starts));
+        assertEquals("that/that/", link.combinePath(ends, ends));
+        assertEquals("/this/that/", link.combinePath(starts, ends));
+        assertEquals("that/this", link.combinePath(ends, starts));
+        assertEquals("/these/these/", link.combinePath(both, both));
+    }
+
+    public @Test void methodAppendPath_Object() throws Exception
+    {
+        LinkTool link = newInstance(LinkTool.PATH_KEY, "/foo");
+        assertEquals("/foo", link.getPath());
+        link.appendPath("bar");
+        assertEquals("/foo/bar", link.getPath());
+        link.appendPath("/bar");
+        assertEquals("/foo/bar/bar", link.getPath());
+        link.setPath("/foo/");
+        link.appendPath("bar/");
+        assertEquals("/foo/bar/", link.getPath());
+        link.appendPath("/bar");
+        assertEquals("/foo/bar/bar", link.getPath());
+    }
+
+    public @Test void methodAppend_Object() throws Exception
+    {
+        LinkTool link = newInstance(LinkTool.PATH_KEY, "/foo");
+        assertEquals("/foo", link.append(null).getPath());
+        link.setPath(null);
+        assertNull(link.getPath());
+        link = link.append("bar");
+        assertEquals("/bar", link.getPath());
+        assertEquals("/bar/foo", link.append("foo").getPath());
+    }
+
+    public @Test void methodGetContextPath() 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());
+    }
+
+    public @Test void methodGetRequestPath() throws Exception
+    {
+        LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
+        assertEquals("/request.vm", link.getRequestPath());
+        link = newInstance("http://foo.com/foo/bar/request.vm?this=that#anc");
+        assertEquals("/request.vm", link.getRequestPath());
+    }
+
+    public @Test void methodGetRoot() throws Exception
+    {
+        LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
+        assertEquals("http://foo.com", link.getRoot());
+        link.setHost("apache.org");
+        assertEquals("https://apache.org", link.secure().getRoot());
+    }
+
+    public @Test void methodGetContextURL() throws Exception
+    {
+        LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
+        assertEquals("http://foo.com/ctx", link.getContextURL());
+        link = newInstance("http://foo.com");
+        assertEquals("http://foo.com", link.getContextURL());
+    }
+
+    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("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/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());
+        link = newInstance("/bar/");
+        assertEquals("/bar/foo/woogie.vm", link.relative("foo/woogie.vm").toString());
+        assertEquals("/bar/yo", link.relative("yo").toString());
+    }
+
+    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);
+        assertTrue(result.isAbsolute());
+        assertEquals("http://apache.org", result.toString());
+        assertEquals(LinkTool.DEFAULT_SCHEME, result.getScheme());
+        assertEquals("apache.org", result.getHost());
+
+        assertFalse(link.isAbsolute());
+        result = link.absolute("/test/foo.vm");
+        assertTrue(result.isAbsolute());
+        assertEquals("/test/foo.vm", result.getPath());
+        assertEquals(null, result.getHost());
+        assertEquals("http:/test/foo.vm", result.toString());
+        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());
+    }
+
+    public @Test void methodGetBaseRef() throws Exception
+    {
+        LinkTool link = newInstance("http://foo.com/ctx/request.vm?this=that#anc");
+        assertEquals("http://foo.com/ctx/request.vm", link.getBaseRef());
+        assertEquals(null, newInstance().getBaseRef());
+    }
+
+    public @Test void methodGetUri() throws Exception
+    {
+        LinkTool link = newInstance(LinkTool.SAFE_MODE_KEY, true);
+        link.setFromURI("http://velocity.apache.org");
+        assertNull(link.getUri());
+        link = newInstance();
+        assertNull(link.getUri());
+        link = link.secure().user("nbubna").host("people.apache.org");
+        assertNotNull(link.getUri());
+        assertEquals("nbubna", link.getUri().getUserInfo());
+    }
+
+    public @Test void methodSetFromURI_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.toString());
+        link.setFromURI("*%&$^%#$*&^!");
+        assertNull(link.toString());
+        link.setFromURI("http://velocity.apache.org");
+        assertNotNull(link.toString());
+        assertEquals("velocity.apache.org", link.getHost());
+    }
+
+    public @Test void methodCreateURI() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertNull(link.createURI());
+        link.setFromURI("http://velocity.apache.org");
+        assertNotNull(link.createURI());
+        link.setPort("foo");
+        assertNull(link.createURI());
+        link.setPort(null);
+        assertTrue(link.setFromURI("mailto:nbubna@apache.org"));
+        assertTrue(link.isOpaque());
+        assertNotNull(link.createURI());
+        assertTrue(link.createURI().isOpaque());
+        assertEquals(link.createURI(), link.createURI());
+        assertFalse(link.createURI() == link.createURI());
+    }
+
+    public @Test void methodUri_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals(null, link.uri(null).toString());
+        assertEquals("http://apache.org?a=b#c", link.uri("http://apache.org?a=b#c").toString());
+        link.setFromURI("https://nbubna@people.apache.org");
+        assertEquals("people.apache.org", link.getHost());
+        assertEquals("https://nbubna@people.apache.org", link.uri(link.createURI()).toString());
+        URI uri = new URI("mailto:nbubna@apache.org");
+        assertEquals("mailto:nbubna@apache.org", link.uri(uri).toString());
+    }
+
+    public @Test void methodSetQuery_Object() throws Exception
+    {
+        LinkTool link = newInstance("/bar?a=b");
+        assertNotNull(link.getQuery());
+        assertEquals("a=b", link.getQuery());
+        link.setQuery("c=d&e=f");
+        assertEquals("c=d&amp;e=f", link.getQuery());
+        link.setXHTML(false);
+        link.setQuery("x=1&amp;y=2");
+        assertEquals("x=1&y=2", link.getQuery());
+        link.setQuery(null);
+        assertEquals(null, link.getQuery());
+    }
+
+    public @Test void methodGetQuery() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals(null, link.getQuery());
+        assertEquals("this=that", link.query("this=that").getQuery());
+    }
+
+    public @Test void methodQuery_Object() throws Exception
+    {
+        LinkTool link = newInstance("https://gmail.com");
+        assertEquals("https://gmail.com", link.query(null).toString());
+        assertEquals("https://gmail.com?v=2", link.query("v=2").toString());
+        link = newInstance("http://go.com?foo=bar");
+        assertEquals("foo=wog", link.query("foo=wog").getQuery());
+        assertEquals("http://go.com", link.query(null).toString());
+    }
+
+    // this method also tests setXHTML
+    public @Test void methodCombineQuery_StringString() throws Exception
+    {
+        LinkTool link = newInstance();
+        String none = null;
+        String empty = "";
+        String test = "test=1";
+        String test2 = "a=b";
+        assertSame(none, link.combineQuery(none, none));
+        assertSame(none, link.combineQuery(none, empty));
+        assertSame(test, link.combineQuery(test, none));
+        assertEquals("test=1", link.combineQuery(test, empty));
+        assertEquals("test=1&amp;a=b", link.combineQuery(test, test2));
+        link.setXHTML(false);
+        assertEquals("a=b&test=1", link.combineQuery(test2, test));
+    }
+
+    public @Test void methodAppendQuery_Object() throws Exception
+    {
+        LinkTool link = newInstance("/foo?bar=woogie");
+        link.appendQuery("x=1");
+        assertEquals("bar=woogie&amp;x=1", link.getQuery());
+        link.appendQuery("y=2");
+        assertEquals("bar=woogie&amp;x=1&amp;y=2", link.getQuery());
+        link.setQuery(null);
+        assertEquals(null, link.getQuery());
+        link.appendQuery("z=3");
+        assertEquals("z=3", link.getQuery());
+    }
+
+    public @Test void methodToQuery_ObjectObject() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals("null=", link.toQuery(null, null));
+        assertEquals("a+b=c", link.toQuery("a b", "c"));
+        assertEquals("x=1", link.toQuery('x', 1));
+        assertEquals("true=false", link.toQuery(true, false));
+        assertEquals("path=%2Ffoo+bar%2Fnew", link.toQuery("path", "/foo bar/new"));
+    }
+
+    public @Test void methodParam_ObjectObject() throws Exception
+    {
+        LinkTool link = newInstance();
+        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 methodParams_Map() throws Exception
+    {
+        LinkTool link = newInstance("http://go.com");
+        Map params = new LinkedHashMap();
+        params.put("this", "that");
+        params.put('x', 1);
+        params.put(true, false);
+        assertEquals("http://go.com?this=that&amp;x=1&amp;true=false", link.params(params).toString());
+        assertEquals("http://go.com", link.params(null).toString());
+        assertEquals("http://go.com", link.params(new HashMap()).toString());
+    }
+
+    public @Test void methodParseQuery_String() throws Exception
+    {
+        LinkTool link = newInstance();
+        Map result = link.parseQuery("a=b&amp;x=1");
+        assertEquals("b", result.get("a"));
+        assertEquals("1", result.get("x"));
+        link.setXHTML(false);
+        result = link.parseQuery("true=false");
+        assertEquals("false", result.get("true"));
+    }
+
+    public @Test void methodGetParams() throws Exception
+    {
+        LinkTool link = newInstance("/foo?a=b&amp;x=true");
+        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"));
+    }
+
+    public @Test void methodSetFragment_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        link.setFragment("foo");
+        assertEquals("#foo", link.toString());
+        link.setFragment(null);
+        assertEquals(null, link.toString());
+        link = newInstance("/foo#bar");
+        link.setFragment("woo gie");
+        assertEquals("/foo#woo%20gie", link.toString());
+    }
+
+    public @Test void methodGetAnchor() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals(null, link.getAnchor());
+        link.setFragment("foo");
+        assertEquals("foo", link.getAnchor());
+        link = newInstance("http://go.com#espn");
+        assertEquals("espn", link.getAnchor());
+        link = newInstance(LinkTool.FRAGMENT_KEY, "foo");
+        assertEquals("foo", link.getAnchor());
+    }
+
+    public @Test void methodAnchor_Object() throws Exception
+    {
+        LinkTool link = newInstance();
+        // here are possible string values to test:
+        String none = null;
+        String empty = "";
+        String space = "a b";
+        String test = "test";
+        assertEquals(null, link.anchor(none).getAnchor());
+        assertEquals(null, link.anchor(empty).getAnchor());
+        assertEquals(test, link.anchor(test).getAnchor());
+        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());
+    }
+
+    public @Test void methodGetSelf() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertSame(link, link.getSelf());
+        assertSame(link, link.uri("http://go.com").getSelf());
+        assertSame(link, link.path("foo").param(1,true).anchor('a').getSelf());
+    }
+
+    public @Test void methodToString() throws Exception
+    {
+        LinkTool link = newInstance();
+        assertEquals(null, link.toString());
+        assertEquals(null, link.secure().toString());
+        assertEquals("http://go.com", link.host("go.com").toString());
+        assertEquals(null, link.port(42).toString());
+        assertEquals("/foo", link.path("foo").toString());
+        assertEquals("?a=1", link.param('a',1).toString());
+        assertEquals("#42", link.anchor(42).toString());
+    }
+
+}
+        
\ No newline at end of file

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java
------------------------------------------------------------------------------
    svn:keywords = Revision

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LinkToolTests.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: velocity/tools/trunk/xdocs/changes.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/xdocs/changes.xml?rev=663715&r1=663714&r2=663715&view=diff
==============================================================================
--- velocity/tools/trunk/xdocs/changes.xml (original)
+++ velocity/tools/trunk/xdocs/changes.xml Thu Jun  5 12:36:05 2008
@@ -35,7 +35,7 @@
         <li>New configuration formats (more concise/flexible/powerful xml, properties, java)</li>
         <li>Entirely new core infrastructure (lazy-loading tools, easier access, standalone support etc)</li>
         <li>Added VelocityViewTag for JSP integration</li>
-        <li>Added DisplayTool, ConversionTool, ClassTool, LoopTool, FieldTool and more</li>
+        <li>Added DisplayTool, ConversionTool, ClassTool, LoopTool, FieldTool, a generic version of LinkTool and more</li>
         <li>Refactored and enhanced a number of existing tools</li>
         <li>Improved documentation</li>
         <li>Deprecated lots of outdated things</li>

Modified: velocity/tools/trunk/xdocs/dependencies.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/xdocs/dependencies.xml?rev=663715&r1=663714&r2=663715&view=diff
==============================================================================
--- velocity/tools/trunk/xdocs/dependencies.xml (original)
+++ velocity/tools/trunk/xdocs/dependencies.xml Thu Jun  5 12:36:05 2008
@@ -65,8 +65,9 @@
               <td>commons-logging</td>
               <td>1.1</td>
               <td>Yes</td>
-              <td>No</td>
-              <td></td>
+              <td>Yes</td>
+              <td>Required for
+                <a href="javadoc/org/apache/velocity/tools/generic/LinkTool.html">LinkTool</a></td>
             </tr>
             <tr>
               <td>velocity</td>

Modified: velocity/tools/trunk/xdocs/generic.project.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/xdocs/generic.project.xml?rev=663715&r1=663714&r2=663715&view=diff
==============================================================================
--- velocity/tools/trunk/xdocs/generic.project.xml (original)
+++ velocity/tools/trunk/xdocs/generic.project.xml Thu Jun  5 12:36:05 2008
@@ -35,6 +35,7 @@
           <item name="DisplayTool"          href="javadoc/org/apache/velocity/tools/generic/DisplayTool.html"/>
           <item name="EscapeTool"           href="javadoc/org/apache/velocity/tools/generic/EscapeTool.html"/>
           <item name="FieldTool"            href="javadoc/org/apache/velocity/tools/generic/FieldTool.html"/>
+          <item name="LinkTool"             href="javadoc/org/apache/velocity/tools/generic/LinkTool.html"/>
           <item name="ListTool"             href="javadoc/org/apache/velocity/tools/generic/ListTool.html"/>
           <item name="LoopTool"             href="javadoc/org/apache/velocity/tools/generic/LoopTool.html"/> />
           <item name="MathTool"             href="javadoc/org/apache/velocity/tools/generic/MathTool.html"/>

Modified: velocity/tools/trunk/xdocs/generic.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/xdocs/generic.xml?rev=663715&r1=663714&r2=663715&view=diff
==============================================================================
--- velocity/tools/trunk/xdocs/generic.xml (original)
+++ velocity/tools/trunk/xdocs/generic.xml Thu Jun  5 12:36:05 2008
@@ -94,6 +94,9 @@
             - A convenience tool to use with #foreach loops. It wraps a list
             with a custom iterator to provide greater control, allowing loops
             to end early, skip ahead and more.</li>
+        <li><a href="javadoc/org/apache/velocity/tools/generic/LinkTool.html">LinkTool</a>
+            - For creating and manipulating URIs and URLs. The API for this tool is
+            designed to closely resemble that of the VelocityView tool of the same name.</li>
         <li><a href="javadoc/org/apache/velocity/tools/generic/ListTool.html">ListTool</a>
             - For working with arrays and lists, treats both transparently
             the same.</li>