/** * 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; } }