/** * 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.activemq.artemis.utils.uri; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Utility class that provides methods for parsing URI's * * This class can be used to split composite URI's into their component parts and is used to extract any * URI options from each URI in order to set specific properties on Beans. * * (copied from activemq 5) */ public class URISupport { /** * A composite URI can be split into one or more CompositeData object which each represent the * individual URIs that comprise the composite one. */ public static class CompositeData { private String host; private String scheme; private String path; private URI[] components; private Map<String, String> parameters; private String fragment; public URI[] getComponents() { return components; } public String getFragment() { return fragment; } public Map<String, String> getParameters() { return parameters; } public String getScheme() { return scheme; } public String getPath() { return path; } public String getHost() { return host; } public URI toURI() throws URISyntaxException { StringBuffer sb = new StringBuffer(); if (scheme != null) { sb.append(scheme); sb.append(':'); } if (host != null && host.length() != 0) { sb.append(host); } else { sb.append('('); for (int i = 0; i < components.length; i++) { if (i != 0) { sb.append(','); } sb.append(components[i].toString()); } sb.append(')'); } if (path != null) { sb.append('/'); sb.append(path); } if (!parameters.isEmpty()) { sb.append("?"); sb.append(createQueryString(parameters)); } if (fragment != null) { sb.append("#"); sb.append(fragment); } return new URI(sb.toString()); } } /** * Give a URI break off any URI options and store them in a Key / Value Mapping. * * @param uri The URI whose query should be extracted and processed. * @return A Mapping of the URI options. * @throws java.net.URISyntaxException */ public static Map<String, String> parseQuery(String uri) throws URISyntaxException { try { uri = uri.substring(uri.lastIndexOf("?") + 1); // get only the relevant part of the query Map<String, String> rc = new HashMap<>(); if (uri != null && !uri.isEmpty()) { parseParameters(rc, uri.split("&")); parseParameters(rc, uri.split(";")); } return rc; } catch (UnsupportedEncodingException e) { throw (URISyntaxException) new URISyntaxException(e.toString(), "Invalid encoding").initCause(e); } } private static void parseParameters(Map<String, String> rc, String[] parameters) throws UnsupportedEncodingException { for (String parameter : parameters) { int p = parameter.indexOf("="); if (p >= 0) { String name = URLDecoder.decode(parameter.substring(0, p), "UTF-8"); String value = URLDecoder.decode(parameter.substring(p + 1), "UTF-8"); rc.put(name, value); } else { rc.put(parameter, null); } } } /** * Given a URI parse and extract any URI query options and return them as a Key / Value mapping. * * This method differs from the {@link parseQuery} method in that it handles composite URI types and * will extract the URI options from the outermost composite URI. * * @param uri The URI whose query should be extracted and processed. * @return A Mapping of the URI options. * @throws java.net.URISyntaxException */ public static Map<String, String> parseParameters(URI uri) throws URISyntaxException { if (!isCompositeURI(uri)) { return uri.getQuery() == null ? emptyMap() : parseQuery(stripPrefix(uri.getQuery(), "?")); } else { CompositeData data = URISupport.parseComposite(uri); Map<String, String> parameters = new HashMap<>(); parameters.putAll(data.getParameters()); if (parameters.isEmpty()) { parameters = emptyMap(); } return parameters; } } /** * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the * newly updated URI that contains the value of the given URI and the appended query value. * * @param uri The source URI that will have the Map entries appended as a URI query value. * @param queryParameters The Key / Value mapping that will be transformed into a URI query string. * @return A new URI value that combines the given URI and the constructed query string. * @throws java.net.URISyntaxException */ public static URI applyParameters(URI uri, Map<String, String> queryParameters) throws URISyntaxException { return applyParameters(uri, queryParameters, ""); } /** * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the * newly updated URI that contains the value of the given URI and the appended query value. Each entry in the query * string is prefixed by the supplied optionPrefix string. * * @param uri The source URI that will have the Map entries appended as a URI query value. * @param queryParameters The Key / Value mapping that will be transformed into a URI query string. * @param optionPrefix A string value that when not null or empty is used to prefix each query option key. * @return A new URI value that combines the given URI and the constructed query string. * @throws java.net.URISyntaxException */ public static URI applyParameters(URI uri, Map<String, String> queryParameters, String optionPrefix) throws URISyntaxException { if (queryParameters != null && !queryParameters.isEmpty()) { StringBuffer newQuery = uri.getRawQuery() != null ? new StringBuffer(uri.getRawQuery()) : new StringBuffer(); for (Map.Entry<String, String> param : queryParameters.entrySet()) { if (param.getKey().startsWith(optionPrefix)) { if (newQuery.length() != 0) { newQuery.append('&'); } final String key = param.getKey().substring(optionPrefix.length()); newQuery.append(key).append('=').append(param.getValue()); } } uri = createURIWithQuery(uri, newQuery.toString()); } return uri; } private static Map<String, String> emptyMap() { return Collections.EMPTY_MAP; } /** * Removes any URI query from the given uri and return a new URI that does not contain the query portion. * * @param uri The URI whose query value is to be removed. * @return a new URI that does not contain a query value. * @throws java.net.URISyntaxException */ public static URI removeQuery(URI uri) throws URISyntaxException { return createURIWithQuery(uri, null); } /** * Creates a URI with the given query, removing an previous query value from the given URI. * * @param uri The source URI whose existing query is replaced with the newly supplied one. * @param query The new URI query string that should be appended to the given URI. * @return a new URI that is a combination of the original URI and the given query string. * @throws java.net.URISyntaxException */ public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { String schemeSpecificPart = uri.getRawSchemeSpecificPart(); // strip existing query if any int questionMark = schemeSpecificPart.lastIndexOf("?"); // make sure question mark is not within parentheses if (questionMark < schemeSpecificPart.lastIndexOf(")")) { questionMark = -1; } if (questionMark > 0) { schemeSpecificPart = schemeSpecificPart.substring(0, questionMark); } if (query != null && query.length() > 0) { schemeSpecificPart += "?" + query; } return new URI(uri.getScheme(), schemeSpecificPart, uri.getFragment()); } /** * Given a composite URI, parse the individual URI elements contained within that URI and return * a CompositeData instance that contains the parsed URI values. * * @param uri The target URI that should be parsed. * @return a new CompositeData instance representing the parsed composite URI. * @throws java.net.URISyntaxException */ public static CompositeData parseComposite(URI uri) throws URISyntaxException { CompositeData rc = new CompositeData(); rc.scheme = uri.getScheme(); String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim(); parseComposite(uri, rc, ssp); rc.fragment = uri.getFragment(); return rc; } /** * Examine a URI and determine if it is a Composite type or not. * * @param uri The URI that is to be examined. * @return true if the given URI is a Composite type. */ public static boolean isCompositeURI(URI uri) { String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim(); if (ssp.indexOf('(') == 0 && checkParenthesis(ssp)) { return true; } return false; } /** * Given a string and a position in that string of an open parend, find the matching close parend. * * @param str The string to be searched for a matching parend. * @param first The index in the string of the opening parend whose close value is to be searched. * @return the index in the string where the closing parend is located. * @throws java.net.URISyntaxException fi the string does not contain a matching parend. */ public static int indexOfParenthesisMatch(String str, int first) throws URISyntaxException { int index = -1; if (first < 0 || first > str.length()) { throw new IllegalArgumentException("Invalid position for first parenthesis: " + first); } if (str.charAt(first) != '(') { throw new IllegalArgumentException("character at indicated position is not a parenthesis"); } int depth = 1; char[] array = str.toCharArray(); for (index = first + 1; index < array.length; ++index) { char current = array[index]; if (current == '(') { depth++; } else if (current == ')') { if (--depth == 0) { break; } } } if (depth != 0) { throw new URISyntaxException(str, "URI did not contain a matching parenthesis."); } return index; } /** * Given a composite URI and a CompositeData instance and the scheme specific part extracted from the source URI, * parse the composite URI and populate the CompositeData object with the results. The source URI is used only * for logging as the ssp should have already been extracted from it and passed here. * * @param uri The original source URI whose ssp is parsed into the composite data. * @param rc The CompositeData instance that will be populated from the given ssp. * @param ssp The scheme specific part from the original string that is a composite or one or more URIs. * @throws java.net.URISyntaxException */ private static void parseComposite(URI uri, CompositeData rc, String ssp) throws URISyntaxException { String componentString; String params; if (!checkParenthesis(ssp)) { throw new URISyntaxException(uri.toString(), "Not a matching number of '(' and ')' parenthesis"); } int p; int initialParen = ssp.indexOf("("); if (initialParen == 0) { rc.host = ssp.substring(0, initialParen); p = rc.host.indexOf("/"); if (p >= 0) { rc.path = rc.host.substring(p); rc.host = rc.host.substring(0, p); } p = indexOfParenthesisMatch(ssp, initialParen); componentString = ssp.substring(initialParen + 1, p); params = ssp.substring(p + 1).trim(); } else { componentString = ssp; params = ""; } String[] components = splitComponents(componentString); rc.components = new URI[components.length]; for (int i = 0; i < components.length; i++) { rc.components[i] = new URI(components[i].trim()); } p = params.indexOf("?"); if (p >= 0) { if (p > 0) { rc.path = stripPrefix(params.substring(0, p), "/"); } rc.parameters = parseQuery(params.substring(p + 1)); } else { if (params.length() > 0) { rc.path = stripPrefix(params, "/"); } rc.parameters = emptyMap(); } } /** * Given the inner portion of a composite URI, split and return each inner URI as a string * element in a new String array. * * @param str The inner URI elements of a composite URI string. * @return an array containing each inner URI from the composite one. */ private static String[] splitComponents(String str) { List<String> l = new ArrayList<>(); int last = 0; int depth = 0; char[] chars = str.toCharArray(); for (int i = 0; i < chars.length; i++) { switch (chars[i]) { case '(': depth++; break; case ')': depth--; break; case ',': if (depth == 0) { String s = str.substring(last, i); l.add(s); last = i + 1; } break; default: } } String s = str.substring(last); if (s.length() != 0) { l.add(s); } String[] rc = new String[l.size()]; l.toArray(rc); return rc; } /** * String the given prefix from the target string and return the result. * * @param value The string that should be trimmed of the given prefix if present. * @param prefix The prefix to remove from the target string. * @return either the original string or a new string minus the supplied prefix if present. */ public static String stripPrefix(String value, String prefix) { if (value.startsWith(prefix)) { return value.substring(prefix.length()); } return value; } /** * Strip a URI of its scheme element. * * @param uri The URI whose scheme value should be stripped. * @return The stripped URI value. * @throws java.net.URISyntaxException */ public static URI stripScheme(URI uri) throws URISyntaxException { return new URI(stripPrefix(uri.getSchemeSpecificPart().trim(), "//")); } /** * Given a key / value mapping, create and return a URI formatted query string that is valid and * can be appended to a URI. Query parameters in the string are sorted by key. * * @param options The Mapping that will create the new Query string. * @return a URI formatted query string. * @throws java.net.URISyntaxException */ public static String createQueryString(Map<String, ? extends Object> options) throws URISyntaxException { try { if (options.size() > 0) { StringBuilder rc = new StringBuilder(); boolean first = true; List<String> keys = new ArrayList<>(); keys.addAll(options.keySet()); Collections.sort(keys); for (String key : keys) { if (first) { first = false; } else { rc.append("&"); } String value = (String) options.get(key); rc.append(URLEncoder.encode(key, "UTF-8")); rc.append("="); rc.append(URLEncoder.encode(value, "UTF-8")); } return rc.toString(); } else { return ""; } } catch (UnsupportedEncodingException e) { throw (URISyntaxException) new URISyntaxException(e.toString(), "Invalid encoding").initCause(e); } } /** * Creates a URI from the original URI and the remaining parameters. * * When the query options of a URI are applied to certain objects the used portion of the query options needs * to be removed and replaced with those that remain so that other parts of the code can attempt to apply the * remainder or give an error is unknown values were given. This method is used to update a URI with those * remainder values. * * @param originalURI The URI whose current parameters are remove and replaced with the given remainder value. * @param params The URI params that should be used to replace the current ones in the target. * @return a new URI that matches the original one but has its query options replaced with the given ones. * @throws java.net.URISyntaxException */ public static URI createRemainingURI(URI originalURI, Map<String, String> params) throws URISyntaxException { String s = createQueryString(params); if (s.length() == 0) { s = null; } return createURIWithQuery(originalURI, s); } /** * Given a URI value create and return a new URI that matches the target one but with the scheme value * supplied to this method. * * @param bindAddr The URI whose scheme value should be altered. * @param scheme The new scheme value to use for the returned URI. * @return a new URI that is a copy of the original except that its scheme matches the supplied one. * @throws java.net.URISyntaxException */ public static URI changeScheme(URI bindAddr, String scheme) throws URISyntaxException { return new URI(scheme, bindAddr.getUserInfo(), bindAddr.getHost(), bindAddr.getPort(), bindAddr.getPath(), bindAddr.getQuery(), bindAddr.getFragment()); } /** * Examine the supplied string and ensure that all parens appear as matching pairs. * * @param str The target string to examine. * @return true if the target string has valid paren pairings. */ public static boolean checkParenthesis(String str) { boolean result = true; if (str != null) { int open = 0; int closed = 0; int i = 0; while ((i = str.indexOf('(', i)) >= 0) { i++; open++; } i = 0; while ((i = str.indexOf(')', i)) >= 0) { i++; closed++; } result = open == closed; } return result; } }