blob: 03bc90691bb5084cc8f9bba9dc7faf38e5a39a5c [file] [log] [blame]
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.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
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 java.util.Set;
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.SkipSetters;
import org.apache.velocity.tools.config.ValidScope;
/**
* <p>The LinkTool provides many methods to work with URIs and can help you:</p>
* <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>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>Template example(s):</p>
* <pre>
* #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>
*
* @author Nathan Bubna
* @since VelocityTools 2.0
* @version $Id: LinkTool.java 601976 2007-12-07 03:50:51Z nbubna $
*/
@DefaultKey("link")
@SkipSetters
@ValidScope(Scope.REQUEST)
public class LinkTool extends SafeConfig implements Cloneable, Serializable
{
private static final long serialVersionUID = -9178414982270037545L;
/** Standard HTML delimiter for query data ('&amp;') */
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 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";
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 String scheme;
protected String user;
protected String host;
protected int port;
protected String path;
protected Map query;
protected String fragment;
protected String charset;
protected String queryDelim;
protected boolean appendParams;
protected boolean forceRelative;
protected boolean opaque;
protected final LinkTool self;
/**
* Default constructor. Tool typically is configured 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;
appendParams = true;
forceRelative = false;
self = this;
}
// --------------------------------------- Setup Methods -------------
/**
* Configuration
*/
protected void configure(ValueParser props)
{
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)
{
setCharacterEncoding(chrst);
}
Boolean xhtml = props.getBoolean(XHTML_MODE_KEY);
if (xhtml != null)
{
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);
}
}
/**
* Equivalent to clone, but with no checked exceptions.
* If for some unfathomable reason clone() doesn't work,
* this will throw a RuntimeException.
* @return new LinkTool
*/
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.
* @param deep whether to make a deep copy
* @return new LinkTool
*/
protected LinkTool duplicate(boolean deep)
{
try
{
LinkTool that = (LinkTool)this.clone();
if (deep && query != null)
{
that.query = new LinkedHashMap(query);
}
return that;
}
catch (CloneNotSupportedException e)
{
String msg = "Could not properly clone " + getClass();
getLog().error(msg, e);
throw new RuntimeException(msg, e);
}
}
public void setCharacterEncoding(String chrst)
{
this.charset = chrst;
}
/**
* <p>Controls the delimiter used for separating query data pairs.
* By default, the standard '&amp;' 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 xhtml if true, the XHTML query data delimiter ('&amp;amp;')
* will be used. if false, then '&amp;' will be used.
* @see <a href="http://www.w3.org/TR/xhtml1/#C_12">Using Ampersands in Attribute Values (and Elsewhere)</a>
*/
public void setXHTML(boolean xhtml)
{
queryDelim = (xhtml) ? XHTML_QUERY_DELIMITER : HTML_QUERY_DELIMITER;
}
/**
* Sets whether or not the {@link #setParam} method
* will override existing query values for the same key or simply append
* the new value to a list of existing values.
* @param addParams whether to add or replace params
*/
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.
* @param forceRelative whether to force a relative URI
*/
public void setForceRelative(boolean forceRelative)
{
this.forceRelative = forceRelative;
}
/**
* This will treat empty strings like null values
* and will trim any trailing ':' character.
* @param obj scheme
*/
public void setScheme(Object obj)
{
if (obj == null)
{
this.scheme = null;
}
else
{
this.scheme = String.valueOf(obj);
if (scheme.length() == 0)
{
this.scheme = null;
}
if (scheme.endsWith(":"))
{
this.scheme = scheme.substring(0, scheme.length() - 1);
}
}
}
/**
* Sets user info
* @param obj user info
*/
public void setUserInfo(Object obj)
{
this.user = obj == null ? null : String.valueOf(obj);
}
/**
Sets host
@param obj host
*/
public void setHost(Object obj)
{
this.host = obj == null ? null : String.valueOf(obj);
}
/**
* If the specified object is null, this will set the port value
* to -1 to indicate that. If it is non-null and cannot be converted
* to an integer, then it will be set to -2 to indicate an error.
* @param obj port
*/
public 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)
{
getLog().error("Could not convert '{}' to int", obj, nfe);
this.port = -2; // use this to mean error
}
}
}
/**
* If this instance is not opaque and the specified value does
* not start with a '/' character, then that will be prepended
* automatically.
* @param obj path
*/
public 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;
}
}
}
/**
* Uses {@link #combinePath} to add the specified value
* to the current {@link #getPath} value. If the specified
* value is null or this instance is opaque, then this is
* a no-op.
* @param obj path
*/
public void appendPath(Object obj)
{
if (obj != null && !this.opaque)
{
setPath(combinePath(getPath(), String.valueOf(obj)));
}
}
/**
* If end is null, this will return start and vice versa.
* If neither is null, this will append the end to the start,
* making sure that there is only one '/' character between
* the two values.
* @param start start path
* @param end end path
* @return combined path
*/
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;
}
}
/**
* 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.
* @param obj query
*/
public void setQuery(Object obj)
{
if (obj == null)
{
this.query = null;
}
else if (obj instanceof Map)
{
this.query = new LinkedHashMap();
for (Map.Entry keyValues : (Set<Map.Entry>)((Map)obj).entrySet())
{
String key = String.valueOf(keyValues.getKey());
Object values = keyValues.getValue();
if (values.getClass().isArray())
{
int size = Array.getLength(values);
switch (size)
{
case 0:
break;
case 1:
this.query.put(key, Array.get(values, 0));
break;
default:
List lst = new ArrayList();
for (int i = 0; i < Array.getLength(values); ++i)
{
lst.add(Array.get(values, i));
}
this.query.put(key, lst);
}
}
else
{
this.query.put(key, values);
}
}
}
else
{
String qs = normalizeQuery(String.valueOf(obj));
this.query = parseQuery(qs);
}
}
/**
* @param qs query string
* @return normalized string
*/
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.
* @param parameters parameters
* @return query string
*/
public String toQuery(Map parameters)
{
if (parameters == null)
{
return null;
}
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()));
}
return query.toString();
}
/**
* Uses {@link #combineQuery} to append the specified value
* to the current {@link #getQuery} value.
* @param obj query to append
*/
public void appendQuery(Object obj)
{
if (obj != null)
{
setQuery(combineQuery(getQuery(), String.valueOf(obj)));
}
}
/**
* 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.
* @param key parameter key
* @param value parameter value
* @param append whether to append parameter to existing ones with same key
*/
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();
putParam(key, value);
}
else if (append)
{
appendParam((String)key, value);
}
else
{
putParam(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);
putParam(key, vals);
}
}
else
{
putParam(key, value);
}
}
private void putParam(Object key, Object value)
{
if (value instanceof Object[])
{
List vals = new ArrayList();
for (Object v : ((Object[])value))
{
vals.add(v);
}
value = vals;
}
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.
* @param obj parameters
* @param append whether to append parameters
*/
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.
* @param key parameter key
* @return parameter value or null
*/
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.
* @param keep <code>false</code> will clear query
*/
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 '&amp;' or '&amp;amp;' at the
* end of the first one, then combine the two, making sure that they
* are separated by only one delimiter.
* @param current current query
* @param add added query
* @return combined query
*/
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 || current.length() == 0)
{
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;
}
/**
* Turns the specified key and value into a properly encoded
* query pair string. If the value is an array or List, then
* this will create a delimited string of query pairs, reusing
* the same key for each of the values separately.
* @param key parameter key
* @param value parameter value(s)
* @return encoded query string fragment
*/
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() */
protected 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);
}
}
}
/**
* 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.
* @param query query string
* @return parameters map
*/
protected Map<String,Object> parseQuery(String query)
{
return parseQuery(normalizeQuery(query), this.queryDelim);
}
/**
* This will use the specified query delimiter to parse the specified
* query string into a map of keys to values.
* If there are multiple query pairs in the string that have the same
* key, then the values will be combined into a single List value
* associated with that key.
* @param query query string
* @param queryDelim query delimiter
* @return parameters map
*/
protected Map<String,Object> parseQuery(String query, String queryDelim)
{
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;
}
/**
* Sets the anchor for this instance and treats empty strings like null.
* @param obj fragment
*/
public void setFragment(Object obj)
{
if (obj == null)
{
this.fragment = null;
}
else
{
this.fragment = String.valueOf(obj);
if (this.fragment.length() == 0)
{
this.fragment = null;
}
}
}
/**
* If the specified value is null, this will set the scheme, userInfo,
* host, port, path, query, and fragment all to their null-equivalent
* values. Otherwise, this will
* convert the specified object into a {@link URI}, then those same
* values from the URI object to this instance, when not null or empty.
* In other words, when given a URI this will only set values present
* in the URI.
* @param obj URI
* @return success
*/
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 = toURI(obj);
if (uri == null)
{
return false;
}
if (uri.getScheme() != null)
{
setScheme(uri.getScheme());
}
if (uri.isOpaque())
{
this.opaque = true;
if (uri.getSchemeSpecificPart() != null)
{
// path is used as scheme-specific part
setPath(uri.getSchemeSpecificPart());
}
}
else
{
if (uri.getUserInfo() != null)
{
setUserInfo(uri.getUserInfo());
}
if (uri.getHost() != null)
{
setHost(uri.getHost());
}
if (uri.getPort() > -1)
{
setPort(uri.getPort());
}
String pth = uri.getPath();
if (pth != null)
{
if (pth.equals("/") || pth.length() == 0)
{
pth = null;
}
setPath(pth);
}
}
if (uri.getQuery() != null)
{
setQuery(uri.getQuery());
}
if (uri.getFragment() != null)
{
setFragment(uri.getFragment());
}
return true;
}
/**
* Turns the specified object into a string and thereby a URI.
* @param obj source object
* @return URI or null
*/
protected URI toURI(Object obj)
{
if (obj instanceof URI)
{
return (URI)obj;
}
else
{
try
{
return new URI(String.valueOf(obj));
}
catch (Exception e)
{
getLog().error("Could not convert '{}' to URI", obj, e);
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.
* @return URI object or null
*/
protected URI createURI()
{
try
{
// fail if there was an error in setting the port
if (port > -2)
{
if (opaque)
{
if (query != null && query.size() > 0)
{
getLog().warn("Ignoring given parameters in opaque URI: {}", getQuery());
}
// path is used as scheme-specific part
return new URI(scheme, path, fragment);
}
else if (forceRelative)
{
if (path == null && query == null && fragment == null)
{
return null;
}
return new URI(null, null, null, -1, path, toQuery(query), fragment);
}
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, toQuery(query), fragment);
}
}
}
catch (Exception e)
{
getLog().error("Could not create URI", e);
}
return null;
}
// --------------------------------------------- Template Methods -----------
/**
* Returns the configured charset used by the {@link #encode} and
* {@link #decode} methods.
* @return character encoding
*/
public String getCharacterEncoding()
{
return this.charset;
}
/**
* Returns true if the query delimiter used by this instance is
* using <code>&amp;amp;</code> as the delimiter for query data pairs
* or just using <code>&amp;</code>.
* @return whether the query string delimiter uses the XHTML delimiter
*/
public boolean isXHTML()
{
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.
* @return whether parameters are appended to the query string
*/
public boolean getAppendParams()
{
return this.appendParams;
}
/**
* Returns a new instance with the specified value set as its scheme.
* @param scheme scheme
* @return new LinkTool
*/
public LinkTool scheme(Object scheme)
{
LinkTool copy = duplicate();
copy.setScheme(scheme);
return copy;
}
/**
* Returns a new instance with the scheme set to "https".
* @return new LinkTool
*/
public LinkTool secure()
{
return scheme(SECURE_SCHEME);
}
/**
* Returns a new instance with the scheme set to "http".
* @return new LinkTool
*/
public LinkTool insecure()
{
return scheme(DEFAULT_SCHEME);
}
/**
* Return the scheme value for this instance.
* @return scheme
*/
public String getScheme()
{
return scheme;
}
/**
* Returns true if this instance's scheme is "https".
* @return whether the protocol is secure
*/
public boolean isSecure()
{
return SECURE_SCHEME.equalsIgnoreCase(getScheme());
}
/**
* Returns true if this instance represents an opaque URI.
* @return opacity flag
* @see URI
*/
public boolean isOpaque()
{
return this.opaque;
}
/**
* Returns a new instance with the specified value
* set as its user info.
* @param info user info
* @return new LinkTool
*/
public LinkTool user(Object info)
{
LinkTool copy = duplicate();
copy.setUserInfo(info);
return copy;
}
/**
* Returns the {@link URI#getUserInfo()} value for this instance.
* @return user info
*/
public String getUser()
{
return this.user;
}
/**
* Returns a new instance with the specified value set as its
* host. If no scheme has yet been set, the new instance will
* also have its scheme set to the {@link #DEFAULT_SCHEME} (http).
* @param host host
* @return new LinkTool
*/
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;
}
/**
* Return the host value for this instance.
* @return host
*/
public String getHost()
{
return this.host;
}
/**
* Returns a new instance with the specified value set
* as its port number. If the value cannot be parsed into
* an integer, the returned instance will always return
* null for {@link #toString} and other
* {@link #createURI}-dependent methods to alert the user
* to the error.
* @param port port
* @return new LinkTool
*/
public LinkTool port(Object port)
{
LinkTool copy = duplicate();
copy.setPort(port);
return copy;
}
/**
* Returns the port value, if any.
* @return port or null
*/
public Integer getPort()
{
if (this.port < 0)
{
return null;
}
return this.port;
}
/**
* Returns a new instance with the specified value
* set as its path.
* @param pth path
* @return new link tool
*/
public LinkTool path(Object pth)
{
LinkTool copy = duplicate();
copy.setPath(pth);
return copy;
}
/**
* Returns the current path value for this instance.
* @return path
*/
public String getPath()
{
return this.path;
}
/**
* Appends the given value to the end of the current
* path value.
* @param pth path to append
* @return new LinkTool
*/
public LinkTool append(Object pth)
{
LinkTool copy = duplicate();
copy.appendPath(pth);
return copy;
}
/**
* Returns the directory stack
* in the set {@link #getPath()} value, by just trimming
* off all that follows the last "/".
* @return directory stack
*/
public String getDirectory()
{
if (this.path == null || this.opaque)
{
return null;
}
int lastSlash = this.path.lastIndexOf('/');
if (lastSlash < 0)
{
return "";
}
return this.path.substring(0, lastSlash + 1);
}
/**
* Returns the last section of the path,
* which is all that follows the final "/".
* @return file
*/
public String getFile()
{
if (this.path == null || this.opaque)
{
return null;
}
int lastSlash = this.path.lastIndexOf('/');
if (lastSlash < 0)
{
return this.path;
}
return this.path.substring(lastSlash + 1, 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 or if there
* was an error when the port value was last set. It will
* return null for any opaque URLs as well, as those have
* no host or port.
* @return root
*/
public String getRoot()
{
LinkTool root = root();
if (root == null)
{
return null;
}
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()
* @return new LinkTool
*/
public LinkTool root()
{
if (host == null || opaque || port == -2)
{
return null;
}
LinkTool copy = absolute();
copy.setPath(null);
copy.setQuery(null);
copy.setFragment(null);
return copy;
}
/**
* Returns a new LinkTool instance with
* the path set to the result of {@link #getDirectory()}
* and the query and fragment set to null.
* @return new LinkTool
*/
public LinkTool directory()
{
LinkTool copy = root();
if (copy == null)
{
copy = duplicate();
// clear query and fragment, since root() didn't
copy.setQuery(null);
copy.setFragment(null);
}
copy.setPath(getDirectory());
return copy;
}
/**
* Returns true if this instance is being forced to
* return relative URIs or has a null scheme value.
* @return whether the link is relative
*/
public boolean isRelative()
{
return (this.forceRelative || this.scheme == null);
}
/**
* Returns a copy of this LinkTool instance that has
* {@link #setForceRelative} set to true.
* @return new LinkTool
*/
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. If the specified relative path is null, that is treated
* the same as an empty path.</p>
*
* <p>Example:
* <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/login/index.vm"&gt;Login Page&lt;/a&gt;</code></p>
*
* @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)
{
LinkTool copy = relative();
// prepend relative paths with the current directory
String pth;
if (obj == null)
{
pth = getContextPath();
}
else
{
pth = combinePath(getContextPath(), String.valueOf(obj));
}
if (copy.setFromURI(pth))
{
return copy;
}
return null;
}
/**
* At this level, this only returns the result of {@link #getDirectory}.
* It is here as an extension hook for subclasses to change the
* "context" for relative links.
* @return context path
* @see #relative(Object)
* @see #getDirectory
*/
public String getContextPath()
{
return getDirectory();
}
/**
* Returns true if this instance has a scheme value
* and is not being forced to create relative URIs.
* @return whether this link is absolute
*/
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.
* @return new LinkTool
*/
public LinkTool absolute()
{
LinkTool copy = duplicate();
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. Unless the specified URI contains a query
* or anchor, those values will not be overwritten when using
* this method.</p>
*
* <p>Example:<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/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>
* </p>
*
* @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();
if (obj == null)
{
// just use the current directory path, if any
copy.setPath(getDirectory());
}
else
{
String pth = String.valueOf(obj);
// paths that don't start with '/'
// are considered relative to the current directory
if (!pth.startsWith(DEFAULT_SCHEME) && !pth.startsWith("/"))
{
pth = combinePath(getDirectory(), pth);
}
if (!copy.setFromURI(pth))
{
return null;
}
}
return copy;
}
/**
* <p>Returns a copy of the link with the given URI reference set.
* Few changes 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 all previous
* settings for scheme, host port, path, query and anchor.</p>
*
* @param uri URI reference to set
* @return a new instance of LinkTool
*/
public LinkTool uri(Object uri)
{
LinkTool copy = duplicate();
if (copy.setFromURI(uri))
{
return copy;
}
return null;
}
/**
* If the tool is not in "safe mode"--which it is by default--
* this will return the {@link URI} representation of this instance,
* if any.
* @return URI
* @see SafeConfig#isSafeMode()
*/
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>
* @return base link
*/
public String getBaseRef()
{
LinkTool copy = duplicate();
copy.setQuery(null);
copy.setFragment(null);
return copy.toString();
}
/**
* Sets the specified value as the current query data,
* after normalizing the pair delimiters. This overrides
* any existing query.
* @param query query string
* @return new LinkTool
*/
public LinkTool query(Object query)
{
LinkTool copy = duplicate();
copy.setQuery(query);
return copy;
}
/**
* Returns the current query as a string, if any.
* @return query string
*/
public String getQuery()
{
return toQuery(this.query);
}
/**
* <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 #getAppendParams} 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
*/
public LinkTool param(Object key, Object value)
{
LinkTool copy = duplicate(true);
copy.setParam(key, value, this.appendParams);
return copy;
}
/**
* Appends a new key=value pair to the existing query
* data.
*
* @param key key of new query parameter
* @param value value of new query parameter
* @return a new instance of LinkTool
*/
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)
{
return this;
}
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.isEmpty())
{
return null;
}
return this.query;
}
/**
* <p>Returns a copy of the link with the specified anchor to be
* added to the end of the generated hyperlink.</p>
*
* <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></p>
*
* @param anchor an internal document reference
* @return a new instance of LinkTool with the set anchor
*/
public LinkTool anchor(Object anchor)
{
LinkTool copy = duplicate();
copy.setFragment(anchor);
return copy;
}
/**
* Returns the anchor (internal document reference) set for this link.
* @return anchor
*/
public String getAnchor()
{
return this.fragment;
}
/**
* @return self
*/
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&amp;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.
* @return string representation
*/
public String toString()
{
URI uri = createURI();
if (uri == null)
{
return null;
}
if (query != null)
{
return decodeQueryPercents(uri.toString());
}
return uri.toString();
}
/**
* This is an ugly (but fast) hack that's needed because URI encodes
* things that we don't need encoded while not encoding things
* that we do need encoded. So, we have to encode query data
* before creating the URI to ensure they are properly encoded,
* but then URI encodes all the % from that encoding. Here,
* we isolate the query data and manually decode the encoded
* %25 in that section back to %, without decoding anything else.
* @param url source url
* @return decoded url
*/
protected String decodeQueryPercents(String url)
{
StringBuilder out = new StringBuilder(url.length());
boolean inQuery = false, havePercent = false, haveTwo = false;
for (int i=0; i<url.length(); i++)
{
char c = url.charAt(i);
if (inQuery)
{
if (havePercent)
{
if (haveTwo)
{
out.append('%');
if (c != '5')
{
out.append('2').append(c);
}
havePercent = haveTwo = false;
}
else if (c == '2')
{
haveTwo = true;
}
else
{
out.append('%').append(c);
havePercent = false;
}
}
else if (c == '%')
{
havePercent = true;
}
else
{
out.append(c);
}
if (c == '#')
{
inQuery = false;
}
}
else
{
out.append(c);
if (c == '?')
{
inQuery = true;
}
}
}
// if things ended part way
if (havePercent)
{
out.append('%');
if (haveTwo)
{
out.append('2');
}
}
return out.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()
* @param obj object to compare to
* @return equality
*/
@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}.
* @return hash code
*/
@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.
* @param obj URL to encode
* @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)
{
getLog().error("Character encoding '{}' is unsupported", charset, uee);
return null;
}
}
/**
* Delegates decoding of the specified url content to
* {@link URLDecoder#decode} using the configured character encoding.
* @param obj URL to decode
* @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)
{
getLog().error("Character encoding '{}' is unsupported", charset, uee);
return null;
}
}
}