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.camel.runtimecatalog;
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.Iterator;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029
030/**
031 * Copied from org.apache.camel.util.URISupport
032 */
033public final class URISupport {
034
035    public static final String RAW_TOKEN_START = "RAW(";
036    public static final String RAW_TOKEN_END = ")";
037
038    private static final String CHARSET = "UTF-8";
039
040    private URISupport() {
041        // Helper class
042    }
043
044    /**
045     * Normalizes the URI so unsafe characters is encoded
046     *
047     * @param uri the input uri
048     * @return as URI instance
049     * @throws URISyntaxException is thrown if syntax error in the input uri
050     */
051    public static URI normalizeUri(String uri) throws URISyntaxException {
052        return new URI(UnsafeUriCharactersEncoder.encode(uri, true));
053    }
054
055    public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
056        Map<String, Object> rc = new LinkedHashMap<>(properties.size());
057
058        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
059            Map.Entry<String, Object> entry = it.next();
060            String name = entry.getKey();
061            if (name.startsWith(optionPrefix)) {
062                Object value = properties.get(name);
063                name = name.substring(optionPrefix.length());
064                rc.put(name, value);
065                it.remove();
066            }
067        }
068
069        return rc;
070    }
071
072    /**
073     * Strips the query parameters from the uri
074     *
075     * @param uri  the uri
076     * @return the uri without the query parameter
077     */
078    public static String stripQuery(String uri) {
079        int idx = uri.indexOf('?');
080        if (idx > -1) {
081            uri = uri.substring(0, idx);
082        }
083        return uri;
084    }
085
086    /**
087     * Parses the query parameters of the uri (eg the query part).
088     *
089     * @param uri the uri
090     * @return the parameters, or an empty map if no parameters (eg never null)
091     * @throws URISyntaxException is thrown if uri has invalid syntax.
092     */
093    public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException {
094        String query = uri.getQuery();
095        if (query == null) {
096            String schemeSpecificPart = uri.getSchemeSpecificPart();
097            int idx = schemeSpecificPart.indexOf('?');
098            if (idx < 0) {
099                // return an empty map
100                return new LinkedHashMap<>(0);
101            } else {
102                query = schemeSpecificPart.substring(idx + 1);
103            }
104        } else {
105            query = stripPrefix(query, "?");
106        }
107        return parseQuery(query);
108    }
109
110    /**
111     * Strips the prefix from the value.
112     * <p/>
113     * Returns the value as-is if not starting with the prefix.
114     *
115     * @param value  the value
116     * @param prefix the prefix to remove from value
117     * @return the value without the prefix
118     */
119    public static String stripPrefix(String value, String prefix) {
120        if (value != null && value.startsWith(prefix)) {
121            return value.substring(prefix.length());
122        }
123        return value;
124    }
125
126    /**
127     * Parses the query part of the uri (eg the parameters).
128     * <p/>
129     * The URI parameters will by default be URI encoded. However you can define a parameter
130     * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value,
131     * and use the value as is (eg key=value) and the value has <b>not</b> been encoded.
132     *
133     * @param uri the uri
134     * @return the parameters, or an empty map if no parameters (eg never null)
135     * @throws URISyntaxException is thrown if uri has invalid syntax.
136     * @see #RAW_TOKEN_START
137     * @see #RAW_TOKEN_END
138     */
139    public static Map<String, Object> parseQuery(String uri) throws URISyntaxException {
140        return parseQuery(uri, false);
141    }
142
143    /**
144     * Parses the query part of the uri (eg the parameters).
145     * <p/>
146     * The URI parameters will by default be URI encoded. However you can define a parameter
147     * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value,
148     * and use the value as is (eg key=value) and the value has <b>not</b> been encoded.
149     *
150     * @param uri the uri
151     * @param useRaw whether to force using raw values
152     * @return the parameters, or an empty map if no parameters (eg never null)
153     * @throws URISyntaxException is thrown if uri has invalid syntax.
154     * @see #RAW_TOKEN_START
155     * @see #RAW_TOKEN_END
156     */
157    public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException {
158        // must check for trailing & as the uri.split("&") will ignore those
159        if (uri != null && uri.endsWith("&")) {
160            throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. "
161                    + "Check the uri and remove the trailing & marker.");
162        }
163
164        if (isEmpty(uri)) {
165            // return an empty map
166            return new LinkedHashMap<>(0);
167        }
168
169        // need to parse the uri query parameters manually as we cannot rely on splitting by &,
170        // as & can be used in a parameter value as well.
171
172        try {
173            // use a linked map so the parameters is in the same order
174            Map<String, Object> rc = new LinkedHashMap<>();
175
176            boolean isKey = true;
177            boolean isValue = false;
178            boolean isRaw = false;
179            StringBuilder key = new StringBuilder();
180            StringBuilder value = new StringBuilder();
181
182            // parse the uri parameters char by char
183            for (int i = 0; i < uri.length(); i++) {
184                // current char
185                char ch = uri.charAt(i);
186                // look ahead of the next char
187                char next;
188                if (i <= uri.length() - 2) {
189                    next = uri.charAt(i + 1);
190                } else {
191                    next = '\u0000';
192                }
193
194                // are we a raw value
195                isRaw = value.toString().startsWith(RAW_TOKEN_START);
196
197                // if we are in raw mode, then we keep adding until we hit the end marker
198                if (isRaw) {
199                    if (isKey) {
200                        key.append(ch);
201                    } else if (isValue) {
202                        value.append(ch);
203                    }
204
205                    // we only end the raw marker if its )& or at the end of the value
206
207                    boolean end = ch == RAW_TOKEN_END.charAt(0) && (next == '&' || next == '\u0000');
208                    if (end) {
209                        // raw value end, so add that as a parameter, and reset flags
210                        addParameter(key.toString(), value.toString(), rc, useRaw || isRaw);
211                        key.setLength(0);
212                        value.setLength(0);
213                        isKey = true;
214                        isValue = false;
215                        isRaw = false;
216                        // skip to next as we are in raw mode and have already added the value
217                        i++;
218                    }
219                    continue;
220                }
221
222                // if its a key and there is a = sign then the key ends and we are in value mode
223                if (isKey && ch == '=') {
224                    isKey = false;
225                    isValue = true;
226                    isRaw = false;
227                    continue;
228                }
229
230                // the & denote parameter is ended
231                if (ch == '&') {
232                    // parameter is ended, as we hit & separator
233                    String aKey = key.toString();
234                    // the key may be a placeholder of options which we then do not know what is
235                    boolean validKey = !aKey.startsWith("{{") && !aKey.endsWith("}}");
236                    if (validKey) {
237                        addParameter(aKey, value.toString(), rc, useRaw || isRaw);
238                    }
239                    key.setLength(0);
240                    value.setLength(0);
241                    isKey = true;
242                    isValue = false;
243                    isRaw = false;
244                    continue;
245                }
246
247                // regular char so add it to the key or value
248                if (isKey) {
249                    key.append(ch);
250                } else if (isValue) {
251                    value.append(ch);
252                }
253            }
254
255            // any left over parameters, then add that
256            if (key.length() > 0) {
257                String aKey = key.toString();
258                // the key may be a placeholder of options which we then do not know what is
259                boolean validKey = !aKey.startsWith("{{") && !aKey.endsWith("}}");
260                if (validKey) {
261                    addParameter(aKey, value.toString(), rc, useRaw || isRaw);
262                }
263            }
264
265            return rc;
266
267        } catch (UnsupportedEncodingException e) {
268            URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
269            se.initCause(e);
270            throw se;
271        }
272    }
273
274    @SuppressWarnings("unchecked")
275    private static void addParameter(String name, String value, Map<String, Object> map, boolean isRaw) throws UnsupportedEncodingException {
276        name = URLDecoder.decode(name, CHARSET);
277        if (!isRaw) {
278            // need to replace % with %25
279            value = URLDecoder.decode(value.replaceAll("%", "%25"), CHARSET);
280        }
281
282        // does the key already exist?
283        if (map.containsKey(name)) {
284            // yes it does, so make sure we can support multiple values, but using a list
285            // to hold the multiple values
286            Object existing = map.get(name);
287            List<String> list;
288            if (existing instanceof List) {
289                list = (List<String>) existing;
290            } else {
291                // create a new list to hold the multiple values
292                list = new ArrayList<>();
293                String s = existing != null ? existing.toString() : null;
294                if (s != null) {
295                    list.add(s);
296                }
297            }
298            list.add(value);
299            map.put(name, list);
300        } else {
301            map.put(name, value);
302        }
303    }
304
305    /**
306     * Assembles a query from the given map.
307     *
308     * @param options  the map with the options (eg key/value pairs)
309     * @param ampersand to use & for Java code, and &amp; for XML
310     * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no options.
311     * @throws URISyntaxException is thrown if uri has invalid syntax.
312     */
313    public static String createQueryString(Map<String, String> options, String ampersand, boolean encode) throws URISyntaxException {
314        try {
315            if (options.size() > 0) {
316                StringBuilder rc = new StringBuilder();
317                boolean first = true;
318                for (Object o : options.keySet()) {
319                    if (first) {
320                        first = false;
321                    } else {
322                        rc.append(ampersand);
323                    }
324
325                    String key = (String) o;
326                    Object value = options.get(key);
327
328                    // use the value as a String
329                    String s = value != null ? value.toString() : null;
330                    appendQueryStringParameter(key, s, rc, encode);
331                }
332                return rc.toString();
333            } else {
334                return "";
335            }
336        } catch (UnsupportedEncodingException e) {
337            URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
338            se.initCause(e);
339            throw se;
340        }
341    }
342
343    private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) throws UnsupportedEncodingException {
344        if (encode) {
345            rc.append(URLEncoder.encode(key, CHARSET));
346        } else {
347            rc.append(key);
348        }
349        // only append if value is not null
350        if (value != null) {
351            rc.append("=");
352            if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) {
353                // do not encode RAW parameters
354                rc.append(value);
355            } else {
356                if (encode) {
357                    rc.append(URLEncoder.encode(value, CHARSET));
358                } else {
359                    rc.append(value);
360                }
361            }
362        }
363    }
364
365    /**
366     * Tests whether the value is <tt>null</tt> or an empty string.
367     *
368     * @param value  the value, if its a String it will be tested for text length as well
369     * @return true if empty
370     */
371    public static boolean isEmpty(Object value) {
372        return !isNotEmpty(value);
373    }
374
375    /**
376     * Tests whether the value is <b>not</b> <tt>null</tt> or an empty string.
377     *
378     * @param value  the value, if its a String it will be tested for text length as well
379     * @return true if <b>not</b> empty
380     */
381    public static boolean isNotEmpty(Object value) {
382        if (value == null) {
383            return false;
384        } else if (value instanceof String) {
385            String text = (String) value;
386            return text.trim().length() > 0;
387        } else {
388            return true;
389        }
390    }
391
392}