org.apache.ode.utils.URITemplate Maven / Gradle / Ivy
/*
* 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.
*/
package org.apache.ode.utils;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.util.URIUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A partial implementation of URI Template expansion
* as specified by the URI template specification.
* Limitations
*
The only operation implemented so far is Var substitution. If an expansion template for another operation (join, neg, opt, etc) is found,
* an {@link UnsupportedOperationException} is thrown.
*
*
* Escaping Considerations
*
Replacement and default values are escaped. All characters except unreserved (as defined by rfc2396) are escaped.
*
unreserved = alphanum | mark
*
mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
*
* Rfc2396 is used to be compliant with {@linkplain java.net.URI java.net.URI}.
*
* Examples:
*
* Given the following template variable names and values:
*
* - foo = tag
* - bar = java
* - name = null
* - date = 2008/05/09
*
* The following URI Templates will be expanded as shown:
*
http://example.com/{foo}/{bar}.{format=xml}
*
http://example.com/tag/java.xml
*
*
http://example.com/tag/java.{format}
*
http://example.com/tag/java.
*
*
http://example.com/{foo}/{name}
*
http://example.com/tag/
*
*
http://example.com/{foo}/{name=james}
*
http://example.com/tag/james
*
*
http://example.org/{date}
*
http://example.org/2008%2F05%2F09
*
*
http://example.org/{-join|&|foo,bar,xyzzy,baz}/{date}
*
--> UnsupportedOperationException
*
* @author Alexis Midon
* @see #varSubstitution(String, Object[], java.util.Map)
*/
public class URITemplate {
private static final Log log = LogFactory.getLog(URITemplate.class);
public static final String EXPANSION_REGEX = "\\{[^\\}]+\\}";
// compiled pattern of the regex
private static final Pattern PATTERN = Pattern.compile(EXPANSION_REGEX);
/**
* Implements the function describes in the spec
*
* @param expansion, an expansion template (with the surrounding braces)
* @return an array of object containing the operation name, the operation argument, a map of
*/
public static Object[] parseExpansion(String expansion) {
// remove surrounding braces if any
if (expansion.matches(EXPANSION_REGEX)) {
expansion = expansion.substring(1, expansion.length() - 1);
}
String[] r;
if (expansion.contains("|")) {
// (op, arg, vars)
r = expansion.split("\\|", -1);
// remove the leading '-' of the operation
r[0] = r[0].substring(1);
} else {
r = new String[]{null, null, expansion};
}
// parse the vars
Map vars = new HashMap();
String[] var = r[2].split(",");
for (String s : var) {
if (s.contains("=")) {
String[] a = s.split("=");
vars.put(a[0], a[1]);
} else {
vars.put(s, null);
}
}
// op, arg, vars
return new Object[]{r[0], r[1], vars};
}
/**
* Simply build a map from nameValuePairs and pass it to {@link #expand(String, java.util.Map)}
*
* @param nameValuePairs an array containing of name, value, name, value, and so on. Null values are allowed.
* @see # expand (String, java.util.Map)
*/
public static String expand(String uriTemplate, String... nameValuePairs) throws URIException, UnsupportedOperationException {
return expand(uriTemplate, toMap(nameValuePairs));
}
/**
* A partial implementation of URI Template expansion
* as specified by the URI template specification.
*
* The only operation implemented as of today is "Var Substitution". If an expansion template for another operation (join, neg, opt, etc) is found,
* an {@link UnsupportedOperationException} will be thrown.
*
* See {@link #varSubstitution(String, Object[], java.util.Map)}
*
* @param uriTemplate the URI template
* @param nameValuePairs a Map of <name, value>. Null values are allowed.
* @return a copy of uri template in which substitutions have been made (if possible)
* @throws URIException if the default protocol charset is not supported
* @throws UnsupportedOperationException if the operation is not supported. Currently only var substitution is supported.
* @see #varSubstitution(String, Object[], java.util.Map)
*/
public static String expand(String uriTemplate, Map nameValuePairs) throws URIException, UnsupportedOperationException {
return expand(uriTemplate, nameValuePairs, false);
}
/**
* Same as {@link #expand(String, java.util.Map)} but preserve an expansion template if the corresponding variable
* is not defined in the {@code nameValuePairs} map (i.e. map.contains(var)==false).
*
Meaning that a template may be returned.
*
If a default value exists for the undefined value, it will be used to replace the expansion pattern.
*
* Beware that this behavior deviates from the URI Template specification.
*
* For instance:
*
Given the following template variable names and values:
*
* - bar = java
* - foo undefined
*
* The following expansion templates will be expanded as shown if {@code preserveUndefinedVar} is true:
*
http://example.com/{bar}
*
http://example.com/java
*
*
{foo=a_default_value}
*
a_default_value
*
*
http://example.com/{bar}/{foo}
*
http://example.com/java/{foo}
*
* @see #expand(String, java.util.Map)
*/
public static String expandLazily(String uriTemplate, Map nameValuePairs) throws URIException, UnsupportedOperationException {
return expand(uriTemplate, nameValuePairs, true);
}
/**
* @see #expandLazily(String, java.util.Map)
*/
public static String expandLazily(String uriTemplate, String... nameValuePairs) throws URIException {
return expandLazily(uriTemplate, toMap(nameValuePairs));
}
/**
* @see #varSubstitution(String, Object[], java.util.Map, boolean)
* @see #expandLazily(String, String[])
*/
private static String expand(String uriTemplate, Map nameValuePairs, boolean preserveUndefinedVar) throws URIException, UnsupportedOperationException {
Matcher m = PATTERN.matcher(uriTemplate);
// Strings are immutable in java
// so let's use a buffer, and append all substrings between 2 matches and the replacement value for each match
StringBuilder sb = new StringBuilder(uriTemplate.length());
int prevEnd = 0;
while (m.find()) {
// append the string between two matches
sb.append(uriTemplate.substring(prevEnd, m.start()));
prevEnd = m.end();
// expansion pattern with braces
String expansionPattern = uriTemplate.substring(m.start(), m.end());
Object[] expansionInfo = parseExpansion(expansionPattern);
String operationName = (String) expansionInfo[0];
// here we have to know which operation apply
if (operationName != null) {
final String msg = "Operation not supported [" + operationName + "]. This expansion pattern [" + expansionPattern + "] is not valid.";
if (log.isWarnEnabled()) log.warn(msg);
throw new UnsupportedOperationException(msg);
} else {
// here we care only for var substitution, i.e expansion patterns with no operation name
sb.append(varSubstitution(expansionPattern, expansionInfo, nameValuePairs, preserveUndefinedVar));
}
}
if (sb.length() == 0) {
// return the template itself if no match (String are immutable in java, no need to clone the template)
return uriTemplate;
} else {
// don't forget the remaining part
sb.append(uriTemplate.substring(prevEnd, uriTemplate.length()));
return sb.toString();
}
}
/**
* An implementation of var substitution as defined by the
* URI template specification.
*
* If for a given variable, the variable is in the name/value map but the associated value is null. The variable will be replaced with an empty string or with the default value if any.
*
* @param expansionPattern an expansion pattern (not a uri template) e.g. "{foo}"
* @param expansionInfo the result of {@link #parseExpansion(String)} for the given expansion pattern
* @param nameValuePairs the Map of names and associated values. May containt null values.
* @return the expanded string, properly escaped.
* @throws URIException if an encoding exception occured
* @see org.apache.commons.httpclient.util.URIUtil#encodeWithinQuery(String)
* @see java.net.URI
*/
public static String varSubstitution(String expansionPattern, Object[] expansionInfo, Map nameValuePairs) throws URIException {
return varSubstitution(expansionPattern, expansionInfo, nameValuePairs, false);
}
/**
* Same as {@link #varSubstitution(String, Object[], java.util.Map)} but the {@code preserveUndefinedVar} boolean
* argument (if {@code true}) allows to preserve an expansion template if the corresponding variable is not defined in the {@code nameValuePairs} map (i.e. map.contains(var)==false).
*
If a default value exists for the undefined value, it will be used to replace the expansion pattern.
*
* Beware that this behavior deviates from the URI Template specification.
*
* For instance:
*
Given the following template variable names and values:
*
* - bar = java
* - foo undefined
*
* The following expansion templates will be expanded as shown if {@code preserveUndefinedVar} is true:
*
{bar}
*
java
*
*
{foo=a_default_value}
*
a_default_value
*
*
{foo}
*
{foo}
*/
public static String varSubstitution(String expansionPattern, Object[] expansionInfo, Map nameValuePairs, boolean preserveUndefinedVar) throws URIException {
Map vars = (Map) expansionInfo[2];
// only one var per pattern
Map.Entry e = (Map.Entry) vars.entrySet().iterator().next();
String var = (String) e.getKey();
String defaultValue = (String) e.getValue();
boolean hasDefaultValue = defaultValue != null;
// this boolean indicates if the var is mentioned in the map, not that the associated value is not null.
boolean varDefined = nameValuePairs.containsKey(var);
String providedValue = nameValuePairs.get(var);
String res;
boolean escapingNeeded = true;
if (varDefined) {
if (providedValue == null && !hasDefaultValue) {
res = "";
} else {
res = providedValue != null ? providedValue : defaultValue;
}
} else {
// If the variable is undefined and no default value is given then substitute with the empty string,
// except if preserveUndefinedVar is true
if (hasDefaultValue) {
res = defaultValue;
} else {
if (preserveUndefinedVar) {
res = expansionPattern;
escapingNeeded = false;
} else {
res = "";
}
}
}
// We assume that the replacement value is for the query part of the URI.
// Actually the query allows less character than the path part. $%&+,:@
// (acording to RFC2396
return escapingNeeded ? URIUtil.encodeWithinQuery(res) : res;
}
private static Map toMap(String... nameValuePairs) {
if (nameValuePairs.length % 2 != 0) {
throw new IllegalArgumentException("An even number of elements is expected.");
}
Map m = new HashMap();
for (int i = 0; i < nameValuePairs.length; i = i + 2) {
m.put(nameValuePairs[i], nameValuePairs[i + 1]);
}
return m;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy