content: /**
   * 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.util;
  
  import java.io.UnsupportedEncodingException;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.net.URLDecoder;
  import java.net.URLEncoder;
  import java.nio.charset.StandardCharsets;
  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.
   */
  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 {
              StringBuilder sb = new StringBuilder();
              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 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<String, String>();
              if (uri != null && !uri.isEmpty()) {
                  String[] parameters = uri.split("&");
                  for (int i = 0; i < parameters.length; i++) {
                      int p = parameters[i].indexOf("=");
                      if (p >= 0) {
                          String name = URLDecoder.decode(parameters[i].substring(0, p), "UTF-8");
                          String value = URLDecoder.decode(parameters[i].substring(p + 1), "UTF-8");
                          rc.put(name, value);
                      } else {
                          rc.put(parameters[i], null);
                      }
                  }
              }
              return rc;
          } catch (UnsupportedEncodingException e) {
              throw (URISyntaxException)new URISyntaxException(e.toString(), "Invalid encoding").initCause(e);
          }
      }
  
      /**
       * 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 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<String, String>();
              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 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 URISyntaxException
       */
      public static URI applyParameters(URI uri, Map<String, String> queryParameters, String optionPrefix) throws URISyntaxException {
          if (queryParameters != null && !queryParameters.isEmpty()) {
              StringBuilder newQuery = uri.getRawQuery() != null ? new StringBuilder(uri.getRawQuery()) : new StringBuilder() ;
              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;
      }
  
      @SuppressWarnings("unchecked")
      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 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 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 CompsoteData instance that contains the parsed URI values.
       *
       * @param uri
       * 		The target URI that should be parsed.
       *
       * @return a new CompsiteData instance representing the parsed composite URI.
       * @throws 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 Compsote 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 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 CompsositeData 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 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<String>();
  
          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 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.
       *
       * @param options
       * 		The Mapping that will create the new Query string.
       *
       * @return a URI formatted query string.
       * @throws URISyntaxException
       */
      public static String createQueryString(Map<String, ? extends Object> options) throws URISyntaxException {
          if (options.size() > 0) {
              StringBuilder rc = new StringBuilder();
              boolean first = true;
              for (String key : options.keySet()) {
                  if (first) {
                      first = false;
                  } else {
                      rc.append("&");
                  }
                  String value = (String)options.get(key);
                  rc.append(URLEncoder.encode(key, StandardCharsets.UTF_8));
                  rc.append("=");
                  rc.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
              }
              return rc.toString();
          } else {
              return "";
          }
      }
  
      /**
       * 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 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 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 parends appear as matching pairs.
       *
       * @param str
       * 		The target string to examine.
       *
       * @return true if the target string has valid parend 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;
      }
  }
  