001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.activemq.util;
018    
019    import java.io.UnsupportedEncodingException;
020    import java.net.URI;
021    import java.net.URISyntaxException;
022    import java.net.URLDecoder;
023    import java.net.URLEncoder;
024    import java.util.ArrayList;
025    import java.util.Collections;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    
030    /**
031     * Utility class that provides methods for parsing URI's
032     *
033     * This class can be used to split composite URI's into their component parts and is used to extract any
034     * URI options from each URI in order to set specific properties on Beans.
035     */
036    public class URISupport {
037    
038        /**
039         * A composite URI can be split into one or more CompositeData object which each represent the
040         * individual URIs that comprise the composite one.
041         */
042        public static class CompositeData {
043            private String host;
044            private String scheme;
045            private String path;
046            private URI components[];
047            private Map<String, String> parameters;
048            private String fragment;
049    
050            public URI[] getComponents() {
051                return components;
052            }
053    
054            public String getFragment() {
055                return fragment;
056            }
057    
058            public Map<String, String> getParameters() {
059                return parameters;
060            }
061    
062            public String getScheme() {
063                return scheme;
064            }
065    
066            public String getPath() {
067                return path;
068            }
069    
070            public String getHost() {
071                return host;
072            }
073    
074            public URI toURI() throws URISyntaxException {
075                StringBuffer sb = new StringBuffer();
076                if (scheme != null) {
077                    sb.append(scheme);
078                    sb.append(':');
079                }
080    
081                if (host != null && host.length() != 0) {
082                    sb.append(host);
083                } else {
084                    sb.append('(');
085                    for (int i = 0; i < components.length; i++) {
086                        if (i != 0) {
087                            sb.append(',');
088                        }
089                        sb.append(components[i].toString());
090                    }
091                    sb.append(')');
092                }
093    
094                if (path != null) {
095                    sb.append('/');
096                    sb.append(path);
097                }
098                if (!parameters.isEmpty()) {
099                    sb.append("?");
100                    sb.append(createQueryString(parameters));
101                }
102                if (fragment != null) {
103                    sb.append("#");
104                    sb.append(fragment);
105                }
106                return new URI(sb.toString());
107            }
108        }
109    
110        /**
111         * Give a URI break off any URI options and store them in a Key / Value Mapping.
112         *
113         * @param uri
114         *          The URI whose query should be extracted and processed.
115         *
116         * @return A Mapping of the URI options.
117         * @throws URISyntaxException
118         */
119        public static Map<String, String> parseQuery(String uri) throws URISyntaxException {
120            try {
121                uri = uri.substring(uri.lastIndexOf("?") + 1); // get only the relevant part of the query
122                Map<String, String> rc = new HashMap<String, String>();
123                if (uri != null && !uri.isEmpty()) {
124                    String[] parameters = uri.split("&");
125                    for (int i = 0; i < parameters.length; i++) {
126                        int p = parameters[i].indexOf("=");
127                        if (p >= 0) {
128                            String name = URLDecoder.decode(parameters[i].substring(0, p), "UTF-8");
129                            String value = URLDecoder.decode(parameters[i].substring(p + 1), "UTF-8");
130                            rc.put(name, value);
131                        } else {
132                            rc.put(parameters[i], null);
133                        }
134                    }
135                }
136                return rc;
137            } catch (UnsupportedEncodingException e) {
138                throw (URISyntaxException)new URISyntaxException(e.toString(), "Invalid encoding").initCause(e);
139            }
140        }
141    
142        /**
143         * Given a URI parse and extract any URI query options and return them as a Key / Value mapping.
144         *
145         * This method differs from the {@link parseQuery} method in that it handles composite URI types and
146         * will extract the URI options from the outermost composite URI.
147         *
148         * @param uri
149         *          The URI whose query should be extracted and processed.
150         *
151         * @return A Mapping of the URI options.
152         * @throws URISyntaxException
153         */
154        public static Map<String, String> parseParameters(URI uri) throws URISyntaxException {
155            if (!isCompositeURI(uri)) {
156                return uri.getQuery() == null ? emptyMap() : parseQuery(stripPrefix(uri.getQuery(), "?"));
157            } else {
158                CompositeData data = URISupport.parseComposite(uri);
159                Map<String, String> parameters = new HashMap<String, String>();
160                parameters.putAll(data.getParameters());
161                if (parameters.isEmpty()) {
162                    parameters = emptyMap();
163                }
164    
165                return parameters;
166            }
167        }
168    
169        /**
170         * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the
171         * newly updated URI that contains the value of the given URI and the appended query value.
172         *
173         * @param uri
174         *          The source URI that will have the Map entries appended as a URI query value.
175         * @param queryParameters
176         *          The Key / Value mapping that will be transformed into a URI query string.
177         *
178         * @return A new URI value that combines the given URI and the constructed query string.
179         * @throws URISyntaxException
180         */
181        public static URI applyParameters(URI uri, Map<String, String> queryParameters) throws URISyntaxException {
182            return applyParameters(uri, queryParameters, "");
183        }
184    
185        /**
186         * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the
187         * newly updated URI that contains the value of the given URI and the appended query value.  Each entry in the query
188         * string is prefixed by the supplied optionPrefix string.
189         *
190         * @param uri
191         *          The source URI that will have the Map entries appended as a URI query value.
192         * @param queryParameters
193         *          The Key / Value mapping that will be transformed into a URI query string.
194         * @param optionPrefix
195         *          A string value that when not null or empty is used to prefix each query option key.
196         *
197         * @return A new URI value that combines the given URI and the constructed query string.
198         * @throws URISyntaxException
199         */
200        public static URI applyParameters(URI uri, Map<String, String> queryParameters, String optionPrefix) throws URISyntaxException {
201            if (queryParameters != null && !queryParameters.isEmpty()) {
202                StringBuffer newQuery = uri.getRawQuery() != null ? new StringBuffer(uri.getRawQuery()) : new StringBuffer() ;
203                for ( Map.Entry<String, String> param: queryParameters.entrySet()) {
204                    if (param.getKey().startsWith(optionPrefix)) {
205                        if (newQuery.length()!=0) {
206                            newQuery.append('&');
207                        }
208                        final String key = param.getKey().substring(optionPrefix.length());
209                        newQuery.append(key).append('=').append(param.getValue());
210                    }
211                }
212                uri = createURIWithQuery(uri, newQuery.toString());
213            }
214            return uri;
215        }
216    
217        @SuppressWarnings("unchecked")
218        private static Map<String, String> emptyMap() {
219            return Collections.EMPTY_MAP;
220        }
221    
222        /**
223         * Removes any URI query from the given uri and return a new URI that does not contain the query portion.
224         *
225         * @param uri
226         *          The URI whose query value is to be removed.
227         *
228         * @return a new URI that does not contain a query value.
229         * @throws URISyntaxException
230         */
231        public static URI removeQuery(URI uri) throws URISyntaxException {
232            return createURIWithQuery(uri, null);
233        }
234    
235        /**
236         * Creates a URI with the given query, removing an previous query value from the given URI.
237         *
238         * @param uri
239         *          The source URI whose existing query is replaced with the newly supplied one.
240         * @param query
241         *          The new URI query string that should be appended to the given URI.
242         *
243         * @return a new URI that is a combination of the original URI and the given query string.
244         * @throws URISyntaxException
245         */
246        public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
247            String schemeSpecificPart = uri.getRawSchemeSpecificPart();
248            // strip existing query if any
249            int questionMark = schemeSpecificPart.lastIndexOf("?");
250            // make sure question mark is not within parentheses
251            if (questionMark < schemeSpecificPart.lastIndexOf(")")) {
252                questionMark = -1;
253            }
254            if (questionMark > 0) {
255                schemeSpecificPart = schemeSpecificPart.substring(0, questionMark);
256            }
257            if (query != null && query.length() > 0) {
258                schemeSpecificPart += "?" + query;
259            }
260            return new URI(uri.getScheme(), schemeSpecificPart, uri.getFragment());
261        }
262    
263        /**
264         * Given a composite URI, parse the individual URI elements contained within that URI and return
265         * a CompsoteData instance that contains the parsed URI values.
266         *
267         * @param uri
268         *          The target URI that should be parsed.
269         *
270         * @return a new CompsiteData instance representing the parsed composite URI.
271         * @throws URISyntaxException
272         */
273        public static CompositeData parseComposite(URI uri) throws URISyntaxException {
274    
275            CompositeData rc = new CompositeData();
276            rc.scheme = uri.getScheme();
277            String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim();
278    
279            parseComposite(uri, rc, ssp);
280    
281            rc.fragment = uri.getFragment();
282            return rc;
283        }
284    
285        /**
286         * Examine a URI and determine if it is a Composite type or not.
287         *
288         * @param uri
289         *          The URI that is to be examined.
290         *
291         * @return true if the given URI is a Compsote type.
292         */
293        public static boolean isCompositeURI(URI uri) {
294            String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim();
295    
296            if (ssp.indexOf('(') == 0 && checkParenthesis(ssp)) {
297                return true;
298            }
299            return false;
300        }
301    
302        /**
303         * Given a string and a position in that string of an open parend, find the matching close parend.
304         *
305         * @param str
306         *          The string to be searched for a matching parend.
307         * @param first
308         *          The index in the string of the opening parend whose close value is to be searched.
309         *
310         * @return the index in the string where the closing parend is located.
311         * @throws URISyntaxException fi the string does not contain a matching parend.
312         */
313        public static int indexOfParenthesisMatch(String str, int first) throws URISyntaxException {
314            int index = -1;
315    
316            if (first < 0 || first > str.length()) {
317                throw new IllegalArgumentException("Invalid position for first parenthesis: " + first);
318            }
319    
320            if (str.charAt(first) != '(') {
321                throw new IllegalArgumentException("character at indicated position is not a parenthesis");
322            }
323    
324            int depth = 1;
325            char[] array = str.toCharArray();
326            for (index = first + 1; index < array.length; ++index) {
327                char current = array[index];
328                if (current == '(') {
329                    depth++;
330                } else if (current == ')') {
331                    if (--depth == 0) {
332                        break;
333                    }
334                }
335            }
336    
337            if (depth != 0) {
338                throw new URISyntaxException(str, "URI did not contain a matching parenthesis.");
339            }
340    
341            return index;
342        }
343    
344        /**
345         * Given a composite URI and a CompositeData instance and the scheme specific part extracted from the source URI,
346         * parse the composite URI and populate the CompositeData object with the results.  The source URI is used only
347         * for logging as the ssp should have already been extracted from it and passed here.
348         *
349         * @param uri
350         *          The original source URI whose ssp is parsed into the composite data.
351         * @param rc
352         *          The CompsositeData instance that will be populated from the given ssp.
353         * @param ssp
354         *          The scheme specific part from the original string that is a composite or one or more URIs.
355         *
356         * @throws URISyntaxException
357         */
358        private static void parseComposite(URI uri, CompositeData rc, String ssp) throws URISyntaxException {
359            String componentString;
360            String params;
361    
362            if (!checkParenthesis(ssp)) {
363                throw new URISyntaxException(uri.toString(), "Not a matching number of '(' and ')' parenthesis");
364            }
365    
366            int p;
367            int initialParen = ssp.indexOf("(");
368            if (initialParen == 0) {
369    
370                rc.host = ssp.substring(0, initialParen);
371                p = rc.host.indexOf("/");
372    
373                if (p >= 0) {
374                    rc.path = rc.host.substring(p);
375                    rc.host = rc.host.substring(0, p);
376                }
377    
378                p = indexOfParenthesisMatch(ssp, initialParen);
379                componentString = ssp.substring(initialParen + 1, p);
380                params = ssp.substring(p + 1).trim();
381    
382            } else {
383                componentString = ssp;
384                params = "";
385            }
386    
387            String components[] = splitComponents(componentString);
388            rc.components = new URI[components.length];
389            for (int i = 0; i < components.length; i++) {
390                rc.components[i] = new URI(components[i].trim());
391            }
392    
393            p = params.indexOf("?");
394            if (p >= 0) {
395                if (p > 0) {
396                    rc.path = stripPrefix(params.substring(0, p), "/");
397                }
398                rc.parameters = parseQuery(params.substring(p + 1));
399            } else {
400                if (params.length() > 0) {
401                    rc.path = stripPrefix(params, "/");
402                }
403                rc.parameters = emptyMap();
404            }
405        }
406    
407        /**
408         * Given the inner portion of a composite URI, split and return each inner URI as a string
409         * element in a new String array.
410         *
411         * @param str
412         *          The inner URI elements of a composite URI string.
413         *
414         * @return an array containing each inner URI from the composite one.
415         */
416        private static String[] splitComponents(String str) {
417            List<String> l = new ArrayList<String>();
418    
419            int last = 0;
420            int depth = 0;
421            char chars[] = str.toCharArray();
422            for (int i = 0; i < chars.length; i++) {
423                switch (chars[i]) {
424                case '(':
425                    depth++;
426                    break;
427                case ')':
428                    depth--;
429                    break;
430                case ',':
431                    if (depth == 0) {
432                        String s = str.substring(last, i);
433                        l.add(s);
434                        last = i + 1;
435                    }
436                    break;
437                default:
438                }
439            }
440    
441            String s = str.substring(last);
442            if (s.length() != 0) {
443                l.add(s);
444            }
445    
446            String rc[] = new String[l.size()];
447            l.toArray(rc);
448            return rc;
449        }
450    
451        /**
452         * String the given prefix from the target string and return the result.
453         *
454         * @param value
455         *          The string that should be trimmed of the given prefix if present.
456         * @param prefix
457         *          The prefix to remove from the target string.
458         *
459         * @return either the original string or a new string minus the supplied prefix if present.
460         */
461        public static String stripPrefix(String value, String prefix) {
462            if (value.startsWith(prefix)) {
463                return value.substring(prefix.length());
464            }
465            return value;
466        }
467    
468        /**
469         * Strip a URI of its scheme element.
470         *
471         * @param uri
472         *          The URI whose scheme value should be stripped.
473         *
474         * @return The stripped URI value.
475         * @throws URISyntaxException
476         */
477        public static URI stripScheme(URI uri) throws URISyntaxException {
478            return new URI(stripPrefix(uri.getSchemeSpecificPart().trim(), "//"));
479        }
480    
481        /**
482         * Given a key / value mapping, create and return a URI formatted query string that is valid and
483         * can be appended to a URI.
484         *
485         * @param options
486         *          The Mapping that will create the new Query string.
487         *
488         * @return a URI formatted query string.
489         * @throws URISyntaxException
490         */
491        public static String createQueryString(Map<String, ? extends Object> options) throws URISyntaxException {
492            try {
493                if (options.size() > 0) {
494                    StringBuffer rc = new StringBuffer();
495                    boolean first = true;
496                    for (String key : options.keySet()) {
497                        if (first) {
498                            first = false;
499                        } else {
500                            rc.append("&");
501                        }
502                        String value = (String)options.get(key);
503                        rc.append(URLEncoder.encode(key, "UTF-8"));
504                        rc.append("=");
505                        rc.append(URLEncoder.encode(value, "UTF-8"));
506                    }
507                    return rc.toString();
508                } else {
509                    return "";
510                }
511            } catch (UnsupportedEncodingException e) {
512                throw (URISyntaxException)new URISyntaxException(e.toString(), "Invalid encoding").initCause(e);
513            }
514        }
515    
516        /**
517         * Creates a URI from the original URI and the remaining parameters.
518         *
519         * When the query options of a URI are applied to certain objects the used portion of the query options needs
520         * to be removed and replaced with those that remain so that other parts of the code can attempt to apply the
521         * remainder or give an error is unknown values were given.  This method is used to update a URI with those
522         * remainder values.
523         *
524         * @param originalURI
525         *          The URI whose current parameters are remove and replaced with the given remainder value.
526         * @param params
527         *          The URI params that should be used to replace the current ones in the target.
528         *
529         * @return a new URI that matches the original one but has its query options replaced with the given ones.
530         * @throws URISyntaxException
531         */
532        public static URI createRemainingURI(URI originalURI, Map<String, String> params) throws URISyntaxException {
533            String s = createQueryString(params);
534            if (s.length() == 0) {
535                s = null;
536            }
537            return createURIWithQuery(originalURI, s);
538        }
539    
540        /**
541         * Given a URI value create and return a new URI that matches the target one but with the scheme value
542         * supplied to this method.
543         *
544         * @param bindAddr
545         *          The URI whose scheme value should be altered.
546         * @param scheme
547         *          The new scheme value to use for the returned URI.
548         *
549         * @return a new URI that is a copy of the original except that its scheme matches the supplied one.
550         * @throws URISyntaxException
551         */
552        public static URI changeScheme(URI bindAddr, String scheme) throws URISyntaxException {
553            return new URI(scheme, bindAddr.getUserInfo(), bindAddr.getHost(), bindAddr.getPort(), bindAddr
554                .getPath(), bindAddr.getQuery(), bindAddr.getFragment());
555        }
556    
557        /**
558         * Examine the supplied string and ensure that all parends appear as matching pairs.
559         *
560         * @param str
561         *          The target string to examine.
562         *
563         * @return true if the target string has valid parend pairings.
564         */
565        public static boolean checkParenthesis(String str) {
566            boolean result = true;
567            if (str != null) {
568                int open = 0;
569                int closed = 0;
570    
571                int i = 0;
572                while ((i = str.indexOf('(', i)) >= 0) {
573                    i++;
574                    open++;
575                }
576                i = 0;
577                while ((i = str.indexOf(')', i)) >= 0) {
578                    i++;
579                    closed++;
580                }
581                result = open == closed;
582            }
583            return result;
584        }
585    }