/*
* 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 licence can also be found at
* http://www.opensource.org/licenses/artistic-license-2.0.php
*/
package org.weborganic.furi;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.weborganic.furi.URICoder;
/**
* A token based on the operators used in PageSeeder.
*
* <p>This syntax borrows heavily from a suggestion made by Roy T. Fielding on the W3C URI list and
* regarding the URI Template draft specification.
*
* <pre>
* instruction = "{" [ operator ] variable-list "}"
* operator = "/" / "+" / ";" / "?" / op-reserve
* variable-list = varspec *( "," varspec )
* varspec = [ var-type ] varname [ ":" prefix-len ] [ "=" default ]
* var-type = "@" / "%" / type-reserve
* varname = ALPHA *( ALPHA | DIGIT | "_" )
* prefix-len = 1*DIGIT
* default = *( unreserved / reserved )
* op-reserve = <anything else that isn't ALPHA or operator>
* type-reserve = <anything else that isn't ALPHA, ",", or operator>
* </pre>
*
* @see <a href="http://lists.w3.org/Archives/Public/uri/2008Sep/0007.html">Re: URI Templates? from
* Roy T. Fielding on 2008-09-16 (uri@w3.org)</a>
*
* @author Christophe Lauret
* @version 6 November 2009
*/
public class TokenOperatorPS extends TokenBase implements TokenOperator, Matchable {
/**
* The pattern for the URI defined pchar:
*
* <pre>
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* pct-encoded = "%" HEXDIG HEXDIG
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
* </pre>
*
* To avoid side-effects with the resolvers non-capturing groups are used.
*
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">Uniform Resource Identifier (URI): Generic
* Syntax</a>
*/
protected static final Pattern PCHAR = Pattern
.compile("(?:[\\w-_.~!$&'()*+,;=:@]|(?:%[0-9a-fA-F]{2}))");
/**
* The list of operators currently supported.
*/
public enum Operator {
/**
* The '?' operator for query parameters.
*
* Example:
*
* <pre>
* undef = null;
* empty = "";
* x = "1024";
* y = "768";
*
* {?x,y} ?x=1024&y=768
* {?x,y,empty} ?x=1024&y=768&empty=
* {?x,y,undef} ?x=1024&y=768
* </pre>
*/
QUERY_PARAMETER('?') {
@Override
public String expand(List<Variable> vars, Parameters parameters) {
if (parameters == null) return "";
StringBuffer expansion = new StringBuffer();
boolean first = true;
for (Variable var : vars) {
if (parameters.exists(var.name())) {
String[] values = var.values(parameters);
for (String value : values) {
expansion.append(first ? '?' : '&');
first = false;
expansion.append(var.name()).append('=').append(URICoder.encode(value));
}
}
}
return expansion.toString();
}
@Override
boolean isResolvable(List<Variable> arg0) {
return true;
}
@Override
boolean resolve(List<Variable> vars, String value, Map<Variable, Object> values) {
for (Variable var : vars) {
Pattern p = Pattern.compile("(?<=[&?]"+var.namePatternString()+"=)([^]*)");
Matcher m = p.matcher(value);
while(m.find()) {
values.put(var, m.group());
}
}
return true;
}
@Override
Pattern pattern(List<Variable> vars) {
StringBuffer pattern = new StringBuffer();
pattern.append("\\?(");
for (Variable var : vars) {
pattern.append('(');
pattern.append(var.namePatternString());
pattern.append("=[^]*)|");
}
pattern.append("&)*");
return Pattern.compile(pattern.toString());
}
},
/**
* The ';' operator for path parameters.
*
* Example:
*
* <pre>
* undef = null;
* empty = "";
* x = "1024";
* y = "768";
*
* {;x,y} ;x=1024;y=768
* {;x,y,empty} ;x=1024;y=768;empty
* {;x,y,undef} ;x=1024;y=768
* </pre>
*/
PATH_PARAMETER(';') {
@Override
String expand(List<Variable> vars, Parameters parameters) {
if (parameters == null) return "";
StringBuffer expansion = new StringBuffer();
for (Iterator<Variable> i = vars.iterator(); i.hasNext();) {
Variable var = i.next();
if (parameters.exists(var.name())) {
String[] values = var.values(parameters);
for (String value : values) {
expansion.append(';');
expansion.append(var.name());
if (value.length() > 0)
expansion.append('=').append(URICoder.encode(value));
}
}
}
return expansion.toString();
}
@Override
boolean isResolvable(List<Variable> vars) {
return true;
}
@Override
boolean resolve(List<Variable> vars, String value, Map<Variable, Object> values) {
for (Variable var : vars) {
Pattern p = Pattern.compile("(?<=;"+var.namePatternString()+"=)([^;/?#]*)");
Matcher m = p.matcher(value);
while(m.find()) {
values.put(var, m.group());
}
}
return true;
}
@Override
Pattern pattern(List<Variable> vars) {
StringBuffer pattern = new StringBuffer();
pattern.append("(?:");
for (Variable var : vars) {
pattern.append("(?:;");
pattern.append(var.namePatternString());
pattern.append("=[^;/?#]*)|");
}
pattern.append(";)*");
return Pattern.compile(pattern.toString());
}
},
/**
* The '/' operator for path segments.
*
* Example:
*
* <pre>
* list = [ "val1", "val2", "val3" ];
* x = "1024";
*
* {/list,x} /val1/val2/val3/1024
* </pre>
*/
PATH_SEGMENT('/') {
@Override
String expand(List<Variable> vars, Parameters parameters) {
if (parameters == null) return "";
StringBuffer expansion = new StringBuffer();
for (Variable var : vars) {
if (parameters.exists(var.name())) {
String[] values = var.values(parameters);
for (String value : values) {
expansion.append('/');
expansion.append(URICoder.encode(value));
}
}
}
return expansion.toString();
}
@Override
boolean isResolvable(List<Variable> arg0) {
return true;
}
@Override
boolean resolve(List<Variable> vars, String value, Map<Variable, Object> values) {
if (vars.size() != 1)
throw new UnsupportedOperationException("Operator + cannot be resolved with multiple variables.");
values.put(vars.get(0), URICoder.decode(value));
return true;
}
@Override
Pattern pattern(List<Variable> vars) {
return Pattern.compile("(?:/[^/?#]*)*");
}
},
/**
* The '+' operator for URI inserts.
*
* Example:
*
* <pre>
* empty = ""
* path = "/foo/bar"
* x = "1024"
*
* {+path}/here /foo/bar/here
* {+path,x}/here /foo/bar,1024/here
* {+path}{x}/here /foo/bar1024/here
* {+empty}/here /here
* </pre>
*/
URI_INSERT('+') {
String expand(List<Variable> vars, Parameters parameters) {
if (parameters == null) return "";
StringBuffer expansion = new StringBuffer();
for (Iterator<Variable> i = vars.iterator(); i.hasNext();) {
Variable var = i.next();
if (parameters.exists(var.name())) {
String[] values = var.values(parameters);
for (String value : values) {
expansion.append(URICoder.minimalEncode(value));
}
}
if (i.hasNext())
expansion.append(',');
}
return expansion.toString();
}
@Override
boolean resolve(List<Variable> vars, String value, Map<Variable, Object> values) {
// TODO: should we return false instead??
if (vars.size() != 1)
throw new UnsupportedOperationException("Operator + cannot be resolved with multiple variables.");
values.put(vars.get(0), URICoder.decode(value));
return true;
}
@Override
boolean isResolvable(List<Variable> vars) {
return vars.size() == 1;
}
@Override
Pattern pattern(List<Variable> vars) {
return Pattern.compile("[^?#]*");
}
};
/**
* The character used to represent this operator.
*/
private final char _c;
/**
* Creates a new operator.
*
* @param c The character used to represent this operator.
*/
private Operator(char c) {
this._c = c;
}
/**
* Returns the character.
*
* @return The character used to represent this operator.
*/
public char character() {
return this._c;
}
/**
* Indicates whether the operator can be resolved.
*
* @param vars The variables for the operator.
*/
abstract boolean isResolvable(List<Variable> vars);
/**
* Apply the expansion rules defined for the operator given the specified argument, variable and
* parameters.
*
* @param vars The variables for the operator.
* @param params The parameters to use.
*/
abstract String expand(List<Variable> vars, Parameters params);
/**
* Returns the pattern for this operator given the specified list of variables.
*
* @param vars The variables for the operator.
*/
abstract Pattern pattern(List<Variable> vars);
/**
* Returns the map of the string to values given the specified data.
*/
abstract boolean resolve(List<Variable> vars, String value, Map<Variable, Object> values);
}
/**
* The operator.
*/
private Operator _operator;
/**
* The variables for this token.
*/
private List<Variable> _vars;
/**
* The pattern for this token.
*/
private Pattern _pattern;
/**
* Creates a new operator token for one variable only.
*
* @param op The operator to use.
* @param var The variable for this operator.
*
* @throws NullPointerException If any of the argument is <code>null</code>.
*/
public TokenOperatorPS(Operator op, Variable var) throws NullPointerException {
super(toExpression(op, var));
if (op == null || var == null)
throw new NullPointerException("The operator must have a value");
this._operator = op;
this._vars = new ArrayList<Variable>(1);
this._vars.add(var);
this._pattern = op.pattern(this._vars);
}
/**
* Creates a new operator token.
*
* @param op The operator to use.
* @param vars The variables for this operator.
*
* @throws NullPointerException If any of the argument is <code>null</code>.
*/
public TokenOperatorPS(Operator op, List<Variable> vars) throws NullPointerException {
super(toExpression(op, vars));
if (op == null || vars == null)
throw new NullPointerException("The operator must have a value");
this._operator = op;
this._vars = vars;
this._pattern = op.pattern(vars);
}
/**
* Expands the token operator using the specified parameters.
*
* @param parameters The parameters for variable substitution.
*
* @return The corresponding expanded string.
*/
public String expand(Parameters parameters) {
return this._operator.expand(this._vars, parameters);
}
/**
* Returns the operator part of this token.
*
* @return the operator.
*/
public Operator operator() {
return this._operator;
}
/**
* Returns the list of variables used in this token.
*
* @return the list of variables.
*/
public List<Variable> variables() {
return this._vars;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isResolvable() {
return this._operator.isResolvable(this._vars);
}
/**
* {@inheritDoc}
*/
public boolean resolve(String expanded, Map<Variable, Object> values) {
if (this.isResolvable()) {
this._operator.resolve(this._vars, expanded, values);
return true;
} else {
return false;
}
}
/**
* {@inheritDoc}
*/
public boolean match(String part) {
return this._pattern.matcher(part).matches();
}
/**
* {@inheritDoc}
*/
public Pattern pattern() {
return this._pattern;
}
/**
* Returns the operator if it is defined in this class.
*
* @param c The character representation of the operator.
*
* @return The corresponding operator instance.
*/
public static Operator toOperator(char c) {
for (Operator o : Operator.values()) {
if (o.character() == c)
return o;
}
return null;
}
/**
* Parses the specified string and returns the corresponding token.
*
* This method accepts both the raw expression or the expression wrapped in curly brackets.
*
* @param exp The expression to parse.
*
* @return The corresponding token.
*
* @throws URITemplateSyntaxException If the string cannot be parsed as a valid
*/
public static TokenOperatorPS parse(String exp) throws URITemplateSyntaxException {
String sexp = strip(exp);
if (sexp.length() < 2)
throw new URITemplateSyntaxException(exp, "Cannot produce a valid token operator.");
char c = sexp.charAt(0);
Operator operator = TokenOperatorPS.toOperator(c);
if (operator == null)
throw new URITemplateSyntaxException(String.valueOf(c), "This operator is not supported");
List<Variable> variables = toVariables(sexp.substring(1));
return new TokenOperatorPS(operator, variables);
}
// private helpers --------------------------------------------------------------------------------
/**
* Generate the expression corresponding to the specified operator and variable.
*
* @param op The operator.
* @param var The variable.
*/
private static String toExpression(Operator op, Variable var) {
return "{"+op.character()+var.name()+'}';
}
/**
* Generate the expression corresponding to the specified operator, argument and variables.
*
* @param op The operator.
* @param vars The variables.
*/
private static String toExpression(Operator op, List<Variable> vars) {
StringBuffer exp = new StringBuffer();
exp.append('{');
exp.append(op.character());
boolean first = true;
for (Variable v : vars) {
if (!first) exp.append(',');
exp.append(v.toString());
first = false;
}
exp.append('}');
return exp.toString();
}
}