/**
* Copyright 2014 Opower, Inc.
* 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 com.opower.rest.client.generator.specimpl;
import com.opower.rest.client.generator.util.Encode;
import com.opower.rest.client.generator.util.PathHelper;
import javax.ws.rs.Path;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
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;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*
*/
public class UriBuilderImpl extends 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() {
UriBuilderImpl impl = new UriBuilderImpl();
impl.host = host;
impl.scheme = scheme;
impl.port = port;
impl.userInfo = userInfo;
impl.path = path;
impl.query = this.query;
impl.fragment = fragment;
impl.ssp = ssp;
return impl;
}
private static final Pattern uriPattern = Pattern.compile("([^:]+)://([^/:]+)(:(\\d+))?(/[^?]*)?(\\?([^#]+))?(#(.*))?");
private static final Pattern pathPattern = Pattern.compile("(/[^?]*)(\\?([^#]+))?(#(.*))?");
/**
* Must follow the patter scheme://host:port/path?query#fragment
* <p/>
* port, path, query and fragment are optional. Scheme and host must be specified.
* <p/>
* You may put path parameters anywhere within the uriTemplate except port
*
* @param uriTemplate
* @return
*/
public static UriBuilder fromTemplate(String uriTemplate) {
UriBuilderImpl impl = new UriBuilderImpl();
impl.uriTemplate(uriTemplate);
return impl;
}
/**
* Must follow the pattern scheme://host:port/path?query#fragment or /path?query#fragment
* <p/>
* port, path, query and fragment are optional. Scheme and host must be specified.
* <p/>
* 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()) {
match = pathPattern.matcher(uriTemplate);
if (!match.matches()) throw new RuntimeException("Illegal uri template: " + uriTemplate);
if (match.group(1) != null) path(match.group(1));
if (match.group(3) != null) replaceQuery(match.group(7));
if (match.group(5) != null) fragment(match.group(8));
} else {
scheme(match.group(1));
host(match.group(2));
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(8));
}
return this;
}
@Override
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) this.query = uri.getRawQuery();
if (uri.getFragment() != null) fragment = uri.getRawFragment();
}
return this;
}
@Override
public UriBuilder scheme(String scheme) throws IllegalArgumentException {
this.scheme = scheme;
return this;
}
@Override
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();
this.query = uri.getRawQuery();
}
return this;
}
@Override
public UriBuilder userInfo(String ui) {
this.userInfo = ui;
return this;
}
@Override
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;
}
@Override
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;
}
@Override
public UriBuilder path(String segment) throws IllegalArgumentException {
if (segment == null) throw new IllegalArgumentException("path was null");
path = paths(true, path, segment);
return this;
}
@SuppressWarnings("unchecked")
@Override
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;
}
@SuppressWarnings("unchecked")
@Override
public UriBuilder path(Class resource, String method) throws IllegalArgumentException {
if (resource == null) throw new IllegalArgumentException("resource was null");
if (method == null) throw new IllegalArgumentException("method was null");
Method theMethod = null;
for (Method m : resource.getMethods()) {
if (m.getName().equals(method)) {
if (theMethod != null && m.isAnnotationPresent(Path.class)) {
throw new IllegalArgumentException("there are two method named " + method);
}
if (m.isAnnotationPresent(Path.class)) theMethod = m;
}
}
return path(theMethod);
}
@Override
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;
}
@Override
public UriBuilder replaceMatrix(String matrix) throws IllegalArgumentException {
if (!matrix.startsWith(";")) matrix = ";" + matrix;
if (path == null) {
path = matrix;
} else {
int start = path.lastIndexOf('/');
if (start < 0) start = 0;
int matrixIndex = path.indexOf(';', start);
if (matrixIndex > -1) path = path.substring(0, matrixIndex) + matrix;
else path += matrix;
}
return this;
}
@Override
public UriBuilder replaceQuery(String query) throws IllegalArgumentException {
if (query == null) {
this.query = null;
return this;
}
this.query = Encode.encodeQueryString(query);
return this;
}
@Override
public UriBuilder fragment(String fragment) throws IllegalArgumentException {
if (fragment != null) {
this.fragment = Encode.encodeFragment(fragment);
}
return this;
}
/**
* Only replace path params in path of URI. This changes state of URIBuilder.
*
* @param name
* @param value
* @param isEncoded
* @return
*/
public UriBuilder substitutePathParam(String name, Object value, boolean isEncoded) {
if (path != null) {
StringBuffer buffer = new StringBuffer();
replacePathParameter(name, value.toString(), isEncoded, path, buffer);
path = buffer.toString();
}
return this;
}
@Override
public URI buildFromMap(Map<String, ? extends Object> values) throws IllegalArgumentException, UriBuilderException {
return buildFromMap(values, false);
}
@Override
public URI buildFromEncodedMap(Map<String, ? extends Object> values) throws IllegalArgumentException, UriBuilderException {
return buildFromMap(values, true);
}
public URI buildFromMap(Map<String, ? extends Object> paramMap, boolean fromEncodedMap) throws IllegalArgumentException, UriBuilderException {
StringBuffer buffer = new StringBuffer();
if (scheme != null) replaceParameter(paramMap, fromEncodedMap, 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, userInfo, buffer).append("@");
if (host != null) replaceParameter(paramMap, fromEncodedMap, host, buffer);
if (port != -1) buffer.append(":").append(Integer.toString(port));
}
if (path != null) {
StringBuffer tmp = new StringBuffer();
replaceParameter(paramMap, fromEncodedMap, 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, query, buffer);
}
if (fragment != null) {
buffer.append("#");
replaceParameter(paramMap, fromEncodedMap, fragment, buffer);
}
String buf = buffer.toString();
try {
return URI.create(buf);
} catch (Exception e) {
throw new RuntimeException("Failed to create URI: " + buf, e);
}
}
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, String string, StringBuffer buffer) {
Matcher matcher = createUriParamMatcher(string);
while (matcher.find()) {
String param = matcher.group(1);
Object valObj = paramMap.get(param);
if (valObj == null) {
throw new IllegalArgumentException("NULL value for template parameter: " + param);
}
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, String string, StringBuffer buffer) {
Matcher matcher = createUriParamMatcher(string);
while (matcher.find()) {
String param = matcher.group(1);
String value = paramMap.get(param).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);
}
}
}
@Override
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);
}
@Override
public UriBuilder matrixParam(String name, Object... values) throws IllegalArgumentException {
if (path == null) path = "";
for (Object val : values) {
path += ";" + Encode.encodeMatrixParam(name) + "=" + Encode.encodeMatrixParam(val.toString());
}
return this;
}
private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter");
@Override
public UriBuilder replaceMatrixParam(String name, Object... values) throws IllegalArgumentException {
if (path == null) return matrixParam(name, values);
// remove all path param expressions so we don't accidentally start replacing within a regular expression
ArrayList<String> pathParams = new ArrayList<String>();
boolean foundParam = false;
Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(PathHelper.replaceEnclosedCurlyBraces(path));
StringBuffer newSegment = new StringBuffer();
while (matcher.find()) {
foundParam = true;
String group = matcher.group();
pathParams.add(PathHelper.recoverEnclosedCurlyBraces(group));
matcher.appendReplacement(newSegment, "_resteasy_uri_parameter");
}
matcher.appendTail(newSegment);
path = newSegment.toString();
// Find last path segment
int start = path.lastIndexOf('/');
if (start < 0) start = 0;
int matrixIndex = path.indexOf(';', start);
if (matrixIndex > -1) {
String matrixParams = path.substring(matrixIndex + 1);
path = path.substring(0, matrixIndex);
MultivaluedMapImpl<String, String> map = new MultivaluedMapImpl<String, String>();
String[] params = matrixParams.split(";");
for (String param : params) {
String[] namevalue = param.split("=");
if (namevalue != null && namevalue.length > 0) {
String theName = namevalue[0];
String value = "";
if (namevalue.length > 1) {
value = namevalue[1];
}
map.add(theName, value);
}
}
map.remove(name);
for (String theName : map.keySet()) {
List<String> vals = map.get(theName);
for (Object val : vals) {
path += ";" + theName + "=" + val.toString();
}
}
}
matrixParam(name, values);
// put back all path param expressions
if (foundParam) {
matcher = PARAM_REPLACEMENT.matcher(path);
newSegment = new StringBuffer();
int i = 0;
while (matcher.find()) {
matcher.appendReplacement(newSegment, pathParams.get(i++));
}
matcher.appendTail(newSegment);
path = newSegment.toString();
}
return this;
}
/**
* Called by ClientRequest.getUri() to add a query parameter for
* {@code @QueryParam} parameters. We do not use UriBuilder.queryParam()
* because
* <ul>
* <li> queryParam() supports URI template processing and this method must
* always encode braces (for parameter substitution is not possible for
* {@code @QueryParam} parameters).
* <p/>
* <li> queryParam() supports "contextual URI encoding" (i.e., it does not
* encode {@code %} characters that are followed by two hex characters).
* The JavaDoc for {@code @QueryParam.value()} explicitly states that
* the value is specified in decoded format and that "any percent
* encoded literals within the value will not be decoded and will
* instead be treated as literal text". This means that it is an
* explicit bug to perform contextual URI encoding of this method's
* name parameter; hence, we must always encode said parameter. This
* method also foregoes contextual URI encoding on this method's value
* parameter because it represents arbitrary data passed to a
* {@code QueryParam} parameter of a client rest (since the client
* rest is nothing more than a transport layer, it should not be
* "interpreting" such data; instead, it should faithfully transmit
* this data over the wire).
* </ul>
*
* @param name the name of the query parameter.
* @param value the value of the query parameter.
* @return Returns this instance to allow call chaining.
*/
public UriBuilder clientQueryParam(String name, Object value) throws IllegalArgumentException {
if (name == null) throw new IllegalArgumentException("name parameter is null");
if (value == null) throw new IllegalArgumentException("A passed in value was null");
if (query == null) query = "";
else query += "&";
query += Encode.encodeQueryParamAsIs(name) + "=" + Encode.encodeQueryParamAsIs(value.toString());
return this;
}
@Override
public UriBuilder queryParam(String name, Object... values) throws IllegalArgumentException {
if (name == null) throw new IllegalArgumentException("name parameter is null");
if (values == null) throw new IllegalArgumentException("values parameter is null");
for (Object value : values) {
if (value == null) throw new IllegalArgumentException("A passed in value was null");
if (this.query == null) this.query = "";
else this.query += "&";
this.query += Encode.encodeQueryParam(name) + "=" + Encode.encodeQueryParam(value.toString());
}
return this;
}
@Override
//CHECKSTYLE:OFF
public UriBuilder replaceQueryParam(String name, Object... values) throws IllegalArgumentException {
//CHECKSTYLE:ON
if (this.query == null || this.query.equals("")) return queryParam(name, values);
String[] params = this.query.split("&");
this.query = null;
String replacedName = Encode.encodeQueryParam(name);
for (String param : params) {
int pos = param.indexOf('=');
if (pos >= 0) {
String paramName = param.substring(0, pos);
if (paramName.equals(replacedName)) continue;
} else {
if (param.equals(replacedName)) continue;
}
if (this.query == null) this.query = "";
else this.query += "&";
this.query += param;
}
// don't set values if values is null
if (values == null) return this;
// don't set values if values is null
if (values == null) return this;
return queryParam(name, values);
}
@Override
//CHECKSTYLE:OFF
public UriBuilder segment(String... segments) throws IllegalArgumentException {
//CHECKSTYLE:ON
if (segments == null) throw new IllegalArgumentException("segments parameter was null");
for (String segment : segments) {
if (segment == null) throw new IllegalArgumentException("A segment is null");
path(Encode.encodePathSegment(segment));
}
return this;
}
@Override
//CHECKSTYLE:OFF
public URI buildFromEncoded(Object... values) throws IllegalArgumentException, UriBuilderException {
//CHECKSTYLE:ON
return buildFromValues(true, values);
}
@Override
public UriBuilder replacePath(String path) {
if (path == null) {
this.path = null;
return this;
}
this.path = Encode.encodePath(path);
return this;
}
}