/*
* Copyright 2014 cruxframework.org.
*
* Licensed 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.cruxframework.crux.core.server.rest.core;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.cruxframework.crux.core.server.rest.util.Encode;
import org.cruxframework.crux.core.server.rest.util.PathHelper;
import org.cruxframework.crux.core.shared.rest.annotation.Path;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UriBuilder
{
private String host;
private String scheme;
private int port = -1;
private String userInfo;
private String path;
private String query;
private String fragment;
private String ssp;
public UriBuilder clone()
{
UriBuilder impl = new UriBuilder();
impl.host = host;
impl.scheme = scheme;
impl.port = port;
impl.userInfo = userInfo;
impl.path = path;
impl.query = query;
impl.fragment = fragment;
impl.ssp = ssp;
return impl;
}
private static final Pattern uriPattern = Pattern.compile("([a-zA-Z0-9+.-]+)://([^/:]+)(:(\\d+))?(/[^?]*)?(\\?([^#]+))?(#(.*))?");
private static final Pattern sspPattern = Pattern.compile("([^:/]+):(.+)");
private static final Pattern pathPattern = Pattern.compile("([^?]*)?(\\?([^#]+))?(#(.*))?");
/**
* You may put path parameters anywhere within the uriTemplate except port
*
* @param uriTemplate
* @return
*/
public UriBuilder uriTemplate(String uriTemplate)
{
Matcher match = uriPattern.matcher(uriTemplate);
if (match.matches())
{
scheme(match.group(1));
String host = match.group(2);
if (host != null)
{
int at = host.indexOf('@');
if (at > -1)
{
String user = host.substring(0, at);
host = host.substring(at + 1);
userInfo(user);
}
}
host(host);
if (match.group(4) != null)
port(Integer.valueOf(match.group(4)));
if (match.group(5) != null)
path(match.group(5));
if (match.group(7) != null)
replaceQuery(match.group(7));
if (match.group(9) != null)
fragment(match.group(9));
return this;
}
match = sspPattern.matcher(uriTemplate);
if (match.matches())
{
scheme(match.group(1));
schemeSpecificPart(match.group(2));
return this;
}
match = pathPattern.matcher(uriTemplate);
if (match.matches())
{
if (match.group(1) != null)
path(match.group(1));
if (match.group(3) != null)
replaceQuery(match.group(3));
if (match.group(5) != null)
fragment(match.group(5));
return this;
}
throw new RuntimeException("Illegal uri template: " + uriTemplate);
}
public UriBuilder uri(String uriTemplate) throws IllegalArgumentException
{
return uriTemplate(uriTemplate);
}
public UriBuilder uri(URI uri) throws IllegalArgumentException
{
if (uri == null)
throw new IllegalArgumentException("URI was null");
if (uri.getScheme() != null)
scheme = uri.getScheme();
if (uri.getRawSchemeSpecificPart() != null && uri.getRawPath() == null)
{
ssp = uri.getRawSchemeSpecificPart();
}
else
{
this.ssp = null;
if (uri.getHost() != null)
host = uri.getHost();
if (uri.getPort() != -1)
port = uri.getPort();
if (uri.getUserInfo() != null)
userInfo = uri.getRawUserInfo();
if (uri.getPath() != null && !uri.getPath().equals(""))
path = uri.getRawPath();
if (uri.getQuery() != null)
query = uri.getRawQuery();
if (uri.getFragment() != null)
fragment = uri.getRawFragment();
}
return this;
}
public UriBuilder scheme(String scheme) throws IllegalArgumentException
{
this.scheme = scheme;
return this;
}
public UriBuilder schemeSpecificPart(String ssp) throws IllegalArgumentException
{
if (ssp == null)
throw new IllegalArgumentException("schemeSpecificPart was null");
StringBuilder sb = new StringBuilder();
if (scheme != null)
sb.append(scheme).append(':');
if (ssp != null)
sb.append(ssp);
if (fragment != null && fragment.length() > 0)
sb.append('#').append(fragment);
URI uri = URI.create(sb.toString());
if (uri.getRawSchemeSpecificPart() != null && uri.getRawPath() == null)
{
this.ssp = uri.getRawSchemeSpecificPart();
}
else
{
this.ssp = null;
userInfo = uri.getRawUserInfo();
host = uri.getHost();
port = uri.getPort();
path = uri.getRawPath();
query = uri.getRawQuery();
}
return this;
}
public UriBuilder userInfo(String ui)
{
this.userInfo = ui;
return this;
}
public UriBuilder host(String host) throws IllegalArgumentException
{
if (host == null)
throw new IllegalArgumentException("schemeSpecificPart was null");
if (host.equals(""))
throw new IllegalArgumentException("invalid host");
this.host = host;
return this;
}
public UriBuilder port(int port) throws IllegalArgumentException
{
if (port < -1)
throw new IllegalArgumentException("Invalid port value");
this.port = port;
return this;
}
protected static String paths(boolean encode, String basePath, String... segments)
{
String path = basePath;
if (path == null)
path = "";
for (String segment : segments)
{
if ("".equals(segment))
continue;
if (path.endsWith("/"))
{
if (segment.startsWith("/"))
{
segment = segment.substring(1);
if ("".equals(segment))
continue;
}
if (encode)
segment = Encode.encodePath(segment);
path += segment;
}
else
{
if (encode)
segment = Encode.encodePath(segment);
if ("".equals(path))
{
path = segment;
}
else if (segment.startsWith("/"))
{
path += segment;
}
else
{
path += "/" + segment;
}
}
}
return path;
}
public UriBuilder path(String segment) throws IllegalArgumentException
{
if (segment == null)
throw new IllegalArgumentException("path was null");
path = paths(true, path, segment);
return this;
}
public UriBuilder path(Class<?> resource) throws IllegalArgumentException
{
if (resource == null)
throw new IllegalArgumentException("path was null");
Path ann = (Path) resource.getAnnotation(Path.class);
if (ann != null)
{
String[] segments = new String[] { ann.value() };
path = paths(true, path, segments);
}
else
{
throw new IllegalArgumentException("Class must be annotated with @Path to invoke path(Class)");
}
return this;
}
public UriBuilder path(Method method) throws IllegalArgumentException
{
if (method == null)
throw new IllegalArgumentException("method was null");
Path ann = method.getAnnotation(Path.class);
if (ann != null)
{
path = paths(true, path, ann.value());
}
else
{
throw new IllegalArgumentException("method is not annotated with @Path");
}
return this;
}
public UriBuilder replaceQuery(String query) throws IllegalArgumentException
{
if (query == null)
{
this.query = null;
return this;
}
this.query = Encode.encodeQueryString(query);
return this;
}
public UriBuilder fragment(String fragment) throws IllegalArgumentException
{
this.fragment = Encode.encodeFragment(fragment);
return this;
}
public URI buildFromMap(Map<String, ? extends Object> paramMap, boolean fromEncodedMap) throws IllegalArgumentException, UriBuilderException
{
String buf = buildString(paramMap, fromEncodedMap, false);
try
{
return URI.create(buf);
}
catch (Exception e)
{
throw new RuntimeException("Failed to create URI: " + buf, e);
}
}
private String buildString(Map<String, ? extends Object> paramMap, boolean fromEncodedMap, boolean isTemplate)
{
StringBuffer buffer = new StringBuffer();
if (scheme != null)
replaceParameter(paramMap, fromEncodedMap, isTemplate, scheme, buffer).append(":");
if (ssp != null)
{
buffer.append(ssp);
}
else if (userInfo != null || host != null || port != -1)
{
buffer.append("//");
if (userInfo != null)
replaceParameter(paramMap, fromEncodedMap, isTemplate, userInfo, buffer).append("@");
if (host != null)
replaceParameter(paramMap, fromEncodedMap, isTemplate, host, buffer);
if (port != -1)
buffer.append(":").append(Integer.toString(port));
}
if (path != null)
{
StringBuffer tmp = new StringBuffer();
replaceParameter(paramMap, fromEncodedMap, isTemplate, path, tmp);
String tmpPath = tmp.toString();
if (userInfo != null || host != null)
{
if (!tmpPath.startsWith("/"))
buffer.append("/");
}
buffer.append(tmpPath);
}
if (query != null)
{
buffer.append("?");
replaceQueryStringParameter(paramMap, fromEncodedMap, isTemplate, query, buffer);
}
if (fragment != null)
{
buffer.append("#");
replaceParameter(paramMap, fromEncodedMap, isTemplate, fragment, buffer);
}
return buffer.toString();
}
protected StringBuffer replacePathParameter(String name, String value, boolean isEncoded, String string, StringBuffer buffer)
{
Matcher matcher = createUriParamMatcher(string);
while (matcher.find())
{
String param = matcher.group(1);
if (!param.equals(name))
continue;
if (!isEncoded)
{
value = Encode.encodePath(value);
}
else
{
value = Encode.encodeNonCodes(value);
}
// if there is a $ then we must backslash it or it will screw up
// regex group substitution
value = value.replace("$", "\\$");
matcher.appendReplacement(buffer, value);
}
matcher.appendTail(buffer);
return buffer;
}
public static Matcher createUriParamMatcher(String string)
{
Matcher matcher = PathHelper.URI_PARAM_PATTERN.matcher(PathHelper.replaceEnclosedCurlyBraces(string));
return matcher;
}
protected StringBuffer replaceParameter(Map<String, ? extends Object> paramMap, boolean fromEncodedMap, boolean isTemplate, String string, StringBuffer buffer)
{
Matcher matcher = createUriParamMatcher(string);
while (matcher.find())
{
String param = matcher.group(1);
Object valObj = paramMap.get(param);
if (valObj == null && !isTemplate)
{
throw new IllegalArgumentException("NULL value for template parameter: " + param);
}
else if (valObj == null && isTemplate)
{
matcher.appendReplacement(buffer, matcher.group());
continue;
}
String value = valObj.toString();
if (value != null)
{
if (!fromEncodedMap)
{
value = Encode.encodePathAsIs(value);
}
else
{
value = Encode.encodePathSaveEncodings(value);
}
matcher.appendReplacement(buffer, Matcher.quoteReplacement(value));
}
else
{
throw new IllegalArgumentException("path param " + param + " has not been provided by the parameter map");
}
}
matcher.appendTail(buffer);
return buffer;
}
protected StringBuffer replaceQueryStringParameter(Map<String, ? extends Object> paramMap, boolean fromEncodedMap, boolean isTemplate, String string, StringBuffer buffer)
{
Matcher matcher = createUriParamMatcher(string);
while (matcher.find())
{
String param = matcher.group(1);
Object valObj = paramMap.get(param);
if (valObj == null && !isTemplate)
{
throw new IllegalArgumentException("NULL value for template parameter: " + param);
}
else if (valObj == null && isTemplate)
{
matcher.appendReplacement(buffer, matcher.group());
continue;
}
String value = valObj.toString();
if (value != null)
{
if (!fromEncodedMap)
{
value = Encode.encodeQueryParamAsIs(value);
}
else
{
value = Encode.encodeQueryParamSaveEncodings(value);
}
matcher.appendReplacement(buffer, value);
}
else
{
throw new IllegalArgumentException("path param " + param + " has not been provided by the parameter map");
}
}
matcher.appendTail(buffer);
return buffer;
}
/**
* Return a unique order list of path params
*
* @return
*/
public List<String> getPathParamNamesInDeclarationOrder()
{
List<String> params = new ArrayList<String>();
HashSet<String> set = new HashSet<String>();
if (scheme != null)
addToPathParamList(params, set, scheme);
if (userInfo != null)
addToPathParamList(params, set, userInfo);
if (host != null)
addToPathParamList(params, set, host);
if (path != null)
addToPathParamList(params, set, path);
if (query != null)
addToPathParamList(params, set, query);
if (fragment != null)
addToPathParamList(params, set, fragment);
return params;
}
private void addToPathParamList(List<String> params, HashSet<String> set, String string)
{
Matcher matcher = PathHelper.URI_PARAM_PATTERN.matcher(PathHelper.replaceEnclosedCurlyBraces(string));
while (matcher.find())
{
String param = matcher.group(1);
if (set.contains(param))
continue;
else
{
set.add(param);
params.add(param);
}
}
}
public URI build(Object... values) throws IllegalArgumentException, UriBuilderException
{
return buildFromValues(false, values);
}
protected URI buildFromValues(boolean encoded, Object... values)
{
List<String> params = getPathParamNamesInDeclarationOrder();
if (values.length < params.size())
throw new IllegalArgumentException("You did not supply enough values to fill path parameters");
Map<String, Object> pathParams = new HashMap<String, Object>();
for (int i = 0; i < params.size(); i++)
{
String pathParam = params.get(i);
Object val = values[i];
if (val == null)
throw new IllegalArgumentException("A value was null");
pathParams.put(pathParam, val.toString());
}
return buildFromMap(pathParams, encoded);
}
public String getHost()
{
return host;
}
public String getScheme()
{
return scheme;
}
public int getPort()
{
return port;
}
public String getUserInfo()
{
return userInfo;
}
public String getPath()
{
return path;
}
public String getQuery()
{
return query;
}
public String getFragment()
{
return fragment;
}
public UriBuilder replacePath(String path)
{
if (path == null)
{
this.path = null;
return this;
}
this.path = Encode.encodePath(path);
return this;
}
/**
* Create a new instance initialized from an existing URI.
*
* @param uri
* a URI that will be used to initialize the UriBuilder.
* @return a new UriBuilder.
* @throws IllegalArgumentException
* if uri is {@code null}.
*/
public static UriBuilder fromUri(URI uri)
{
return new UriBuilder().uri(uri);
}
/**
* Create a new instance initialized from an existing URI.
*
* @param uriTemplate
* a URI template that will be used to initialize the UriBuilder,
* may contain URI parameters.
* @return a new UriBuilder.
* @throws IllegalArgumentException
* if {@code uriTemplate} is not a valid URI template or is
* {@code null}.
*/
public static UriBuilder fromUri(String uriTemplate)
{
return new UriBuilder().uri(uriTemplate);
}
}