All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.util.URISupport Maven / Gradle / Ivy

There is a newer version: 4.6.0
Show newest version
/**
 * 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.camel.util;

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.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * URI utilities.
 *
 * @version 
 */
public final class URISupport {

    public static final String RAW_TOKEN_START = "RAW(";
    public static final String RAW_TOKEN_END = ")";

    // Match any key-value pair in the URI query string whose key contains
    // "passphrase" or "password" or secret key (case-insensitive).
    // First capture group is the key, second is the value.
    private static final Pattern SECRETS = Pattern.compile("([?&][^=]*(?:passphrase|password|secretKey)[^=]*)=([^&]*)",
            Pattern.CASE_INSENSITIVE);
    
    // Match the user password in the URI as second capture group
    // (applies to URI with authority component and userinfo token in the form "user:password").
    private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*:)(.*)(@)");
    
    // Match the user password in the URI path as second capture group
    // (applies to URI path with authority component and userinfo token in the form "user:password").
    private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*:)(.*)(@)");
    
    private static final String CHARSET = "UTF-8";

    private URISupport() {
        // Helper class
    }

    /**
     * Removes detected sensitive information (such as passwords) from the URI and returns the result.
     *
     * @param uri The uri to sanitize.
     * @see #SECRETS and #USERINFO_PASSWORD for the matched pattern
     *
     * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey sanitized.
     */
    public static String sanitizeUri(String uri) {
        // use xxxxx as replacement as that works well with JMX also
        String sanitized = uri;
        if (uri != null) {
            sanitized = SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx");
            sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
        }
        return sanitized;
    }
    
    /**
     * Removes detected sensitive information (such as passwords) from the
     * path part of an URI (that is, the part without the query
     * parameters or component prefix) and returns the result.
     * 
     * @param path the URI path to sanitize
     * @return null if the path is null, otherwise the sanitized path
     */
    public static String sanitizePath(String path) {
        String sanitized = path;
        if (path != null) {
            sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
        }
        return sanitized;
    }

    /**
     * Extracts the scheme specific path from the URI that is used as the remainder option when creating endpoints.
     *
     * @param u      the URI
     * @param useRaw whether to force using raw values
     * @return the remainder path
     */
    public static String extractRemainderPath(URI u, boolean useRaw) {
        String path = useRaw ? u.getRawSchemeSpecificPart() : u.getSchemeSpecificPart();

        // lets trim off any query arguments
        if (path.startsWith("//")) {
            path = path.substring(2);
        }
        int idx = path.indexOf('?');
        if (idx > -1) {
            path = path.substring(0, idx);
        }

        return path;
    }

    /**
     * Parses the query part of the uri (eg the parameters).
     * 

* The URI parameters will by default be URI encoded. However you can define a parameter * values with the syntax: key=RAW(value) which tells Camel to not encode the value, * and use the value as is (eg key=value) and the value has not been encoded. * * @param uri the uri * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ public static Map parseQuery(String uri) throws URISyntaxException { return parseQuery(uri, false); } /** * Parses the query part of the uri (eg the parameters). *

* The URI parameters will by default be URI encoded. However you can define a parameter * values with the syntax: key=RAW(value) which tells Camel to not encode the value, * and use the value as is (eg key=value) and the value has not been encoded. * * @param uri the uri * @param useRaw whether to force using raw values * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ public static Map parseQuery(String uri, boolean useRaw) throws URISyntaxException { return parseQuery(uri, useRaw, false); } /** * Parses the query part of the uri (eg the parameters). *

* The URI parameters will by default be URI encoded. However you can define a parameter * values with the syntax: key=RAW(value) which tells Camel to not encode the value, * and use the value as is (eg key=value) and the value has not been encoded. * * @param uri the uri * @param useRaw whether to force using raw values * @param lenient whether to parse lenient and ignore trailing & markers which has no key or value which can happen when using HTTP components * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ public static Map parseQuery(String uri, boolean useRaw, boolean lenient) throws URISyntaxException { // must check for trailing & as the uri.split("&") will ignore those if (!lenient) { if (uri != null && uri.endsWith("&")) { throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + "Check the uri and remove the trailing & marker."); } } if (uri == null || ObjectHelper.isEmpty(uri)) { // return an empty map return new LinkedHashMap(0); } // need to parse the uri query parameters manually as we cannot rely on splitting by &, // as & can be used in a parameter value as well. try { // use a linked map so the parameters is in the same order Map rc = new LinkedHashMap(); boolean isKey = true; boolean isValue = false; boolean isRaw = false; StringBuilder key = new StringBuilder(); StringBuilder value = new StringBuilder(); // parse the uri parameters char by char for (int i = 0; i < uri.length(); i++) { // current char char ch = uri.charAt(i); // look ahead of the next char char next; if (i <= uri.length() - 2) { next = uri.charAt(i + 1); } else { next = '\u0000'; } // are we a raw value isRaw = value.toString().startsWith(RAW_TOKEN_START); // if we are in raw mode, then we keep adding until we hit the end marker if (isRaw) { if (isKey) { key.append(ch); } else if (isValue) { value.append(ch); } // we only end the raw marker if its )& or at the end of the value boolean end = ch == RAW_TOKEN_END.charAt(0) && (next == '&' || next == '\u0000'); if (end) { // raw value end, so add that as a parameter, and reset flags addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); key.setLength(0); value.setLength(0); isKey = true; isValue = false; isRaw = false; // skip to next as we are in raw mode and have already added the value i++; } continue; } // if its a key and there is a = sign then the key ends and we are in value mode if (isKey && ch == '=') { isKey = false; isValue = true; isRaw = false; continue; } // the & denote parameter is ended if (ch == '&') { // parameter is ended, as we hit & separator addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); key.setLength(0); value.setLength(0); isKey = true; isValue = false; isRaw = false; continue; } // regular char so add it to the key or value if (isKey) { key.append(ch); } else if (isValue) { value.append(ch); } } // any left over parameters, then add that if (key.length() > 0) { addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); } return rc; } catch (UnsupportedEncodingException e) { URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); se.initCause(e); throw se; } } private static void addParameter(String name, String value, Map map, boolean isRaw) throws UnsupportedEncodingException { name = URLDecoder.decode(name, CHARSET); if (!isRaw) { // need to replace % with %25 String s = StringHelper.replaceAll(value, "%", "%25"); value = URLDecoder.decode(s, CHARSET); } // does the key already exist? if (map.containsKey(name)) { // yes it does, so make sure we can support multiple values, but using a list // to hold the multiple values Object existing = map.get(name); List list; if (existing instanceof List) { list = CastUtils.cast((List) existing); } else { // create a new list to hold the multiple values list = new ArrayList(); String s = existing != null ? existing.toString() : null; if (s != null) { list.add(s); } } list.add(value); map.put(name, list); } else { map.put(name, value); } } /** * Parses the query parameters of the uri (eg the query part). * * @param uri the uri * @return the parameters, or an empty map if no parameters (eg never null) * @throws URISyntaxException is thrown if uri has invalid syntax. */ public static Map parseParameters(URI uri) throws URISyntaxException { String query = uri.getQuery(); if (query == null) { String schemeSpecificPart = uri.getSchemeSpecificPart(); int idx = schemeSpecificPart.indexOf('?'); if (idx < 0) { // return an empty map return new LinkedHashMap(0); } else { query = schemeSpecificPart.substring(idx + 1); } } else { query = stripPrefix(query, "?"); } return parseQuery(query); } /** * Traverses the given parameters, and resolve any parameter values which uses the RAW token * syntax: key=RAW(value). This method will then remove the RAW tokens, and replace * the content of the value, with just the value. * * @param parameters the uri parameters * @see #parseQuery(String) * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ @SuppressWarnings("unchecked") public static void resolveRawParameterValues(Map parameters) { for (Map.Entry entry : parameters.entrySet()) { if (entry.getValue() != null) { // if the value is a list then we need to iterate Object value = entry.getValue(); if (value instanceof List) { List list = (List) value; for (int i = 0; i < list.size(); i++) { Object obj = list.get(i); if (obj != null) { String str = obj.toString(); if (str.startsWith(RAW_TOKEN_START) && str.endsWith(RAW_TOKEN_END)) { str = str.substring(4, str.length() - 1); // update the string in the list list.set(i, str); } } } } else { String str = entry.getValue().toString(); if (str.startsWith(RAW_TOKEN_START) && str.endsWith(RAW_TOKEN_END)) { str = str.substring(4, str.length() - 1); entry.setValue(str); } } } } } /** * Creates a URI with the given query * * @param uri the uri * @param query the query to append to the uri * @return uri with the query appended * @throws URISyntaxException is thrown if uri has invalid syntax. */ public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { ObjectHelper.notNull(uri, "uri"); // assemble string as new uri and replace parameters with the query instead String s = uri.toString(); String before = ObjectHelper.before(s, "?"); if (before == null) { before = ObjectHelper.before(s, "#"); } if (before != null) { s = before; } if (query != null) { s = s + "?" + query; } if ((!s.contains("#")) && (uri.getFragment() != null)) { s = s + "#" + uri.getFragment(); } return new URI(s); } /** * Strips the prefix from the value. *

* Returns the value as-is if not starting with the prefix. * * @param value the value * @param prefix the prefix to remove from value * @return the value without the prefix */ public static String stripPrefix(String value, String prefix) { if (value != null && value.startsWith(prefix)) { return value.substring(prefix.length()); } return value; } /** * Assembles a query from the given map. * * @param options the map with the options (eg key/value pairs) * @return a query string with key1=value&key2=value2&..., or an empty string if there is no options. * @throws URISyntaxException is thrown if uri has invalid syntax. */ @SuppressWarnings("unchecked") public static String createQueryString(Map options) throws URISyntaxException { try { if (options.size() > 0) { StringBuilder rc = new StringBuilder(); boolean first = true; for (Object o : options.keySet()) { if (first) { first = false; } else { rc.append("&"); } String key = (String) o; Object value = options.get(key); // the value may be a list since the same key has multiple values if (value instanceof List) { List list = (List) value; for (Iterator it = list.iterator(); it.hasNext();) { String s = it.next(); appendQueryStringParameter(key, s, rc); // append & separator if there is more in the list to append if (it.hasNext()) { rc.append("&"); } } } else { // use the value as a String String s = value != null ? value.toString() : null; appendQueryStringParameter(key, s, rc); } } return rc.toString(); } else { return ""; } } catch (UnsupportedEncodingException e) { URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); se.initCause(e); throw se; } } private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException { rc.append(URLEncoder.encode(key, CHARSET)); // only append if value is not null if (value != null) { rc.append("="); if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) { // do not encode RAW parameters unless it has % // need to replace % with %25 to avoid losing "%" when decoding String s = StringHelper.replaceAll(value, "%", "%25"); rc.append(s); } else { rc.append(URLEncoder.encode(value, CHARSET)); } } } /** * Creates a URI from the original URI and the remaining parameters *

* Used by various Camel components */ public static URI createRemainingURI(URI originalURI, Map params) throws URISyntaxException { String s = createQueryString(params); if (s.length() == 0) { s = null; } return createURIWithQuery(originalURI, s); } /** * Appends the given parameters to the given URI. *

* It keeps the original parameters and if a new parameter is already defined in * {@code originalURI}, it will be replaced by its value in {@code newParameters}. * * @param originalURI the original URI * @param newParameters the parameters to add * @return the URI with all the parameters * @throws URISyntaxException is thrown if the uri syntax is invalid * @throws UnsupportedEncodingException is thrown if encoding error */ public static String appendParametersToURI(String originalURI, Map newParameters) throws URISyntaxException, UnsupportedEncodingException { URI uri = new URI(normalizeUri(originalURI)); Map parameters = parseParameters(uri); parameters.putAll(newParameters); return createRemainingURI(uri, parameters).toString(); } /** * Normalizes the uri by reordering the parameters so they are sorted and thus * we can use the uris for endpoint matching. *

* The URI parameters will by default be URI encoded. However you can define a parameter * values with the syntax: key=RAW(value) which tells Camel to not encode the value, * and use the value as is (eg key=value) and the value has not been encoded. * * @param uri the uri * @return the normalized uri * @throws URISyntaxException in thrown if the uri syntax is invalid * @throws UnsupportedEncodingException is thrown if encoding error * @see #RAW_TOKEN_START * @see #RAW_TOKEN_END */ public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); String path = u.getSchemeSpecificPart(); String scheme = u.getScheme(); // not possible to normalize if (scheme == null || path == null) { return uri; } // lets trim off any query arguments if (path.startsWith("//")) { path = path.substring(2); } int idx = path.indexOf('?'); // when the path has ? if (idx != -1) { path = path.substring(0, idx); } if (u.getScheme().startsWith("http")) { path = UnsafeUriCharactersEncoder.encodeHttpURI(path); } else { path = UnsafeUriCharactersEncoder.encode(path); } // okay if we have user info in the path and they use @ in username or password, // then we need to encode them (but leave the last @ sign before the hostname) // this is needed as Camel end users may not encode their user info properly, but expect // this to work out of the box with Camel, and hence we need to fix it for them String userInfoPath = path; if (userInfoPath.contains("/")) { userInfoPath = userInfoPath.substring(0, userInfoPath.indexOf("/")); } if (StringHelper.countChar(userInfoPath, '@') > 1) { int max = userInfoPath.lastIndexOf('@'); String before = userInfoPath.substring(0, max); // after must be from original path String after = path.substring(max); // replace the @ with %40 before = StringHelper.replaceAll(before, "@", "%40"); path = before + after; } // in case there are parameters we should reorder them Map parameters = URISupport.parseParameters(u); if (parameters.isEmpty()) { // no parameters then just return return buildUri(scheme, path, null); } else { // reorder parameters a..z List keys = new ArrayList(parameters.keySet()); Collections.sort(keys); Map sorted = new LinkedHashMap(parameters.size()); for (String key : keys) { sorted.put(key, parameters.get(key)); } // build uri object with sorted parameters String query = URISupport.createQueryString(sorted); return buildUri(scheme, path, query); } } private static String buildUri(String scheme, String path, String query) { // must include :// to do a correct URI all components can work with return scheme + "://" + path + (query != null ? "?" + query : ""); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy