/* * This file is part of the URI Template library. * * For licensing information please see the file license.txt included in the release. * A copy of this license can also be found at * http://www.opensource.org/licenses/artistic-license-2.0.php */ package org.weborganic.furi; import java.util.regex.Pattern; /** * A variable in a URL pattern or template. * * The variables can be typed by prefixing the variable name. Types are not required, if no type is * specified, the variable is considered untyped. * * Note: there is no predefined list of types as the handling of types is out of scope. The syntax * simply allows variables to be associated with a type. * * Examples of variables: * <pre> * foo - An untyped variable named 'foo' * bar - An untyped variable named 'bar' * ping:foo - A variable named 'foo' typed 'ping' * ping:foo=1 - A variable named 'foo' typed 'ping' which default value is '1' * foo=pong - An untyped variable named 'foo' which default value is 'pong' * </pre> * * Variables only appear in the context of the a template expansion. * * Expansion rule (4.4.1): * * <pre> * "In a variable ('var') expansion, if the variable is defined then substitute the value of * the variable, otherwise substitute the default value. * If no default value is given then substitute with the empty string." * </pre> * * Syntax for variables: * <pre> * var = [ vartype ":" ] varname [ "=" vardefault ] * vars = var [ *("," var) ] * vartype = (ALPHA / DIGIT)* (ALPHA / DIGIT / "." / "_" / "-" ) * varname = (ALPHA / DIGIT)* (ALPHA / DIGIT / "." / "_" / "-" ) * vardefault = *(unreserved / pct-encoded) * </pre> * * @see <a * href="http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html">URI * Template (Internet Draft 3)</a> * @see <a href="http://tools.ietf.org/html/rfc3986">RFC 3986 - Uniform Resource Identifier (URI): * Generic Syntax<a/> * * @author Christophe Lauret * @version 11 June 2009 */ public class Variable { /** * Used for reserved variable names. */ public enum Reserved { /** * The wildcard represented by the 'asterisk' */ WILDCARD("*"); /** * The symbol for this reserved. */ private String _symbol; /** * Construct a new reserved variable - keep it private. * * @param symbol The symbol used for this reserved variable name. */ private Reserved(String symbol) { this._symbol = symbol; } /** * @return the symbol used for this reserved variable name. */ String symbol() { return this._symbol; } }; /** * Indicate that the variable's value should be processed as a list ("@") or an associative array ("%"). * * This variable type is an instruction for the template processor. * It is not an indication of language or implementation type. */ public enum Form { /** * Indicate that this variable can be expanded as a simple string (default). */ STRING, /** * Indicate that this variable can be expanded as a list of strings. */ LIST, /** * Indicates that this variable can be expanded as an associated array. */ MAP; /** * Returns the type of this variable from the specified expression. * * <p> * This method does not return <code>null</code> * * @param exp The expression. * @return The type of this expression. */ protected static Form getType(String exp) { if (exp.length() == 0) return STRING; char c = exp.charAt(0); if (c == '@') return LIST; if (c == '%') return MAP; return STRING; } } /** * Indicate that the variable's value should be processed as a list ("@") or an associative array ("%"). * * This variable type is an instruction for the template processor. * It is not an indication of language or implementation type. */ public enum Modifier { /** * Indicate that this variable can be expanded as a simple string (default). */ SUBSTRING, /** * Indicate that this variable can be expanded as a list of strings. */ REMAINDER; } /** * The pattern for a valid variable name. */ private static final Pattern VALID_NAME = Pattern.compile("[a-zA-Z0-9][\\w.-]*"); /** * The pattern for a valid normalised variable value: any unreserved character or an escape * sequence. This pattern contains non-capturing parentheses to make it easier to get variable * values as a group. */ protected static final Pattern VALID_VALUE = Pattern.compile("(?:[\\w.~-]|(?:%[0-9a-fA-F]{2}))+"); /** * The default value is an empty string. */ private static final String DEFAULT_VALUE = ""; /** * The type of this variable. */ private Form _form = Form.STRING; /** * The implementation type of this variable (eg. string, integer, etc... can be user-defined). * * <p> * Use <code>null</code> for untyped. */ private VariableType _type; /** * The name of this variable. */ private String _name; /** * The default value for this variable. */ private String _default; /** * Creates a new untyped reserved variable. * * @param reserved The name of the variable. * * @throws NullPointerException If the specified name is <code>null</code>. * @throws IllegalArgumentException If the specified name is an empty string. */ public Variable(Reserved reserved) throws NullPointerException, IllegalArgumentException { this._name = reserved.symbol(); this._default = DEFAULT_VALUE; this._form = Form.STRING; this._type = null; } /** * Creates a new untyped variable. * * @param name The name of the variable. * * @throws NullPointerException If the specified name is <code>null</code>. * @throws IllegalArgumentException If the specified name is an empty string. */ public Variable(String name) throws NullPointerException, IllegalArgumentException { this(name, DEFAULT_VALUE); } /** * Creates a new untyped variable. * * @param name The name of the variable. * @param def The default value for the variable. * * @throws NullPointerException If the specified name is <code>null</code>. * @throws IllegalArgumentException If the specified name is an empty string. */ public Variable(String name, String def) throws NullPointerException, IllegalArgumentException { this(name, def, null); } /** * Creates a new variable. * * @param name The name of the variable. * @param def The default value for the variable. * @param type The type of the variable. * * @throws NullPointerException If the specified name is <code>null</code>. * @throws IllegalArgumentException If the specified name is an empty string. */ public Variable(String name, String def, VariableType type) throws NullPointerException, IllegalArgumentException { if (name == null) throw new NullPointerException("A variable must have a name, but was null"); if (!isValidName(name)) throw new IllegalArgumentException("The variable name is not valid: " + name); this._name = name; this._default = def != null ? def : DEFAULT_VALUE; this._type = type; this._form = Form.getType(name); } /** * Creates a new variable. * * @param name The name of the variable. * @param def The default value for the variable. * @param type The type of the variable. * * @throws NullPointerException If the specified name is <code>null</code>. * @throws IllegalArgumentException If the specified name is an empty string. */ public Variable(String name, String def, VariableType type, Form form) throws NullPointerException, IllegalArgumentException { if (name == null) throw new NullPointerException("A variable must have a name, but was null"); if (!isValidName(name)) throw new IllegalArgumentException("The variable name is not valid: " + name); this._name = name; this._default = def != null ? def : DEFAULT_VALUE; this._type = type; this._form = form != null? form : Form.STRING; } /** * Returns the form of this variable. * * <p> * This method will never return <code>null</code>. * * @return The form of this variable. */ public Form form() { return this._form; } /** * Returns the name of this variable. * * <p> * This method never return <code>null</code>. * * @return The name of this variable. */ public String name() { return this._name; } /** * Returns the default value for this variable. * * This method never return <code>null</code>. * * @return The default value for this variable. */ public String defaultValue() { return this._default; } /** * Returns the implementation type of this variable. * * <p> * This method will return <code>null</code> if the variable is untyped. * * @return The type of this variable. */ public VariableType type() { return this._type; } /** * Returns the expanded value of this variable. * * If no value is specified for this variable, the default value is returned instead. * * @param parameters The parameters. * * @return The value. */ public String value(Parameters parameters) { // No parameters: use the default value if (parameters == null) return this._default; // Defined and non-empty: return the first value in a list String[] values = parameters.getValues(this._name); if (values != null && values.length > 0 && values[0] != null) { return values[0]; // Empty or undefined: return the default } else { return this._default; } } /** * Returns the expanded value of this variable. * * If no values are specified for this variable, the default value is returned instead. * * @param parameters The parameters. * * @return The values. */ public String[] values(Parameters parameters) { // No parameters: use the default value if (parameters == null) return new String[] { this._default }; String[] values = parameters.getValues(this._name); // Defined and non-empty: return the values if (values != null && values.length > 0 && values[0].length() > 0) { return values; // Empty or undefined: return the default } else { return new String[] { this._default }; } } /** * {@inheritDoc} */ @Override public boolean equals(Object o) { if (o == this) return true; if ((o == null) || (o.getClass() != this.getClass())) return false; Variable v = (Variable) o; // name and default cannot be null return _name.equals(v._name) && _default.equals(v._default); } /** * {@inheritDoc} */ @Override public int hashCode() { return this._name.hashCode() + 7 * this._default.hashCode(); } /** * {@inheritDoc} */ @Override public String toString() { if (this._default.length() > 0) return this._name + '=' + this._default; else return this._name; } // Static helpers // ============================================================================================== /** * Parses the given expression and returns the corresponding instance. * * @param exp The expression to parse. * * @return the corresponding variable. * * @throws URITemplateSyntaxException If the expression cannot be parsed */ public static Variable parse(String exp) throws URITemplateSyntaxException { // Capture the form if any Form f = Form.getType(exp); if (f != Form.STRING) exp = exp.substring(1); int colon = exp.indexOf(':'); // untyped if (colon < 0) { Variable v = parseUntyped(exp); v._form = f; return v; // ignore the empty type and treat as untyped } else if (colon == 0) { Variable v = parseUntyped(exp.substring(1)); v._form = f; return v; // a type is specified } else { Variable v = parseUntyped(exp.substring(colon + 1)); v._type = new VariableType(exp.substring(0, colon)); v._form = f; return v; } } /** * Parses the given expression and returns the corresponding instance. * * @param exp The expression to parse. * * @return the corresponding variable. * * @throws URITemplateSyntaxException If the expression cannot be parsed */ private static Variable parseUntyped(String exp) throws URITemplateSyntaxException { int equal = exp.indexOf('='); if (equal == 0) throw new URITemplateSyntaxException(exp, "Variable name is empty string"); if (equal > 0) { return new Variable(exp.substring(0, equal), exp.substring(equal + 1)); } else { return new Variable(exp, null); } } /** * Indicates whether the variable has a valid name according to the specifications. * * @param name The name of the variable. * * @return <code>true</code> if the name is valid; <code>false</code> otherwise. */ public static boolean isValidName(String name) { if (name == null) return false; return VALID_NAME.matcher(name).matches(); } /** * Indicates whether the variable has a valid value according to the specifications. * * @param value The value of the variable. * * @return <code>true</code> if the name is not valid; <code>false</code> otherwise. */ public static boolean isValidValue(String value) { if (value == null) return false; return VALID_VALUE.matcher(value).matches(); } // helpers ------------------------------------------------------------------- /** * Returns the name of this variable as a regular expression pattern string for use in a regular * expression. * * <p> * Implementation note: this method replaces any character that could be interpreted as a regex * meta-character, it is more efficient than using quotation (\Q...\E) for the whole string. * * @return The regex pattern corresponding to this name. */ protected String namePatternString() { return this._name.indexOf('.') < 0 ? this._name : this.name().replaceAll("\\.", "\\\\."); } }