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 */
017package org.apache.activemq.util;
018
019import java.io.UnsupportedEncodingException;
020import java.net.URI;
021import java.net.URISyntaxException;
022import java.net.URLDecoder;
023import java.net.URLEncoder;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.List;
028import 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 */
036public 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}