001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Anahide Tchertchian
018 *     Florent Guillaume
019 *     Kevin Leturc <kleturc@nuxeo.com>
020 */
021package org.nuxeo.common.utils;
022
023import static java.nio.charset.StandardCharsets.UTF_8;
024
025import java.io.UnsupportedEncodingException;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URLDecoder;
029import java.net.URLEncoder;
030import java.util.ArrayList;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037
038/**
039 * Helper class to parse a URI or build one given parameters.
040 *
041 * @author Anahide Tchertchian
042 * @author Florent Guillaume
043 */
044public final class URIUtils {
045
046    private static final Log log = LogFactory.getLog(URIUtils.class);
047
048    // This is an utility class.
049    private URIUtils() {
050    }
051
052    /**
053     * Creates an URI query given the request parameters.
054     *
055     * @return an URI query given the request parameters.
056     */
057    public static String getURIQuery(Map<String, String> parameters) {
058        String query = null;
059        if (parameters != null) {
060            try {
061                List<String> items = new ArrayList<>();
062                for (Map.Entry<String, String> paramInfo : parameters.entrySet()) {
063                    String key = paramInfo.getKey();
064                    String value = paramInfo.getValue();
065                    if (key != null) {
066                        if (value == null) {
067                            value = "";
068                        }
069                        items.add(String.format("%s=%s", URLEncoder.encode(key, UTF_8.name()),
070                                URLEncoder.encode(value, UTF_8.name())));
071                    }
072                }
073                query = String.join("&", items);
074            } catch (UnsupportedEncodingException e) {
075                log.error("Failed to get uri query", e);
076            }
077        }
078        return query;
079    }
080
081    /**
082     * Returns an URI path given the uri.
083     */
084    public static String getURIPath(String uri) {
085        if (uri == null) {
086            return null;
087        }
088        String path = uri;
089        int index = uri.indexOf('?');
090        if (index != -1) {
091            path = uri.substring(0, index);
092        }
093        return path;
094    }
095
096    /**
097     * @return a map with request parameters information given an URI query.
098     */
099    public static Map<String, String> getRequestParameters(String uriQuery) {
100        Map<String, String> parameters = null;
101        if (uriQuery != null && uriQuery.length() > 0) {
102            try {
103                String strippedQuery;
104                int index = uriQuery.indexOf('?');
105                if (index != -1) {
106                    strippedQuery = uriQuery.substring(index + 1);
107                } else {
108                    strippedQuery = uriQuery;
109                }
110                String[] items = strippedQuery.split("&");
111                if (items.length > 0) {
112                    parameters = new LinkedHashMap<>();
113                    for (String item : items) {
114                        String[] param = item.split("=");
115                        if (param.length == 2) {
116                            parameters.put(URLDecoder.decode(param[0], "UTF-8"),
117                                    URLDecoder.decode(param[1], "UTF-8"));
118                        } else if (param.length == 1) {
119                            parameters.put(URLDecoder.decode(param[0], "UTF-8"), null);
120                        }
121                    }
122                }
123            } catch (UnsupportedEncodingException e) {
124                log.error("Failed to get request parameters from uri", e);
125            }
126        }
127        return parameters;
128    }
129
130    public static String addParametersToURIQuery(String uriString, Map<String, String> parameters) {
131        if (uriString == null || parameters == null || parameters.isEmpty()) {
132            return uriString;
133        }
134        String uriPath = getURIPath(uriString);
135        Map<String, String> existingParams = getRequestParameters(uriString.substring(uriPath.length()));
136        if (existingParams == null) {
137            existingParams = new LinkedHashMap<>();
138        }
139        existingParams.putAll(parameters);
140        String res;
141        if (!existingParams.isEmpty()) {
142            String newQuery = getURIQuery(existingParams);
143            res = uriPath + '?' + newQuery;
144        } else {
145            res = uriPath;
146        }
147        return res;
148    }
149
150    public static String quoteURIPathComponent(String s, boolean quoteSlash) {
151        return quoteURIPathComponent(s, quoteSlash, true);
152    }
153
154    /**
155     * Quotes a URI path component, with ability to quote "/" and "@" characters or not depending on the URI path
156     *
157     * @since 5.6
158     * @param s the uri path to quote
159     * @param quoteSlash true if "/" character should be quoted and replaced by %2F
160     * @param quoteAt true if "@" character should be quoted and replaced by %40
161     */
162    public static String quoteURIPathComponent(String s, boolean quoteSlash, boolean quoteAt) {
163        return quoteURIPathComponent(s, quoteSlash ? "%2F" : null, quoteAt ? "%40" : null);
164    }
165
166    /**
167     * Quotes a URI path token. For example, a blob filename. It replaces "/" by "-".
168     *
169     * @since 7.3
170     * @param s the uri path token to quote
171     */
172    public static String quoteURIPathToken(String s) {
173        return quoteURIPathComponent(s, "-", "%40");
174    }
175
176    /**
177     * Quotes a URI path component, with ability to quote "/" and "@" characters or not depending on the URI path
178     *
179     * @since 5.6
180     * @param s the uri path to quote
181     * @param slashSequence if null, do not quote "/", otherwise, replace "/" by the given sequence
182     * @param atSequence if null, do not quote "@", otherwise, replace "@" by the given sequence
183     */
184    protected static String quoteURIPathComponent(String s, String slashSequence, String atSequence) {
185        if ("".equals(s)) {
186            return s;
187        }
188        URI uri;
189        try {
190            // fake scheme so that a colon is not mistaken as a scheme
191            uri = new URI("x", s, null);
192        } catch (URISyntaxException e) {
193            throw new IllegalArgumentException("Illegal characters in: " + s, e);
194        }
195        String r = uri.toASCIIString().substring(2);
196        // replace reserved characters ;:$&+=?/[]@
197        // FIXME: find a better way to do this...
198        r = r.replace(";", "%3B");
199        r = r.replace(":", "%3A");
200        r = r.replace("$", "%24");
201        r = r.replace("&", "%26");
202        r = r.replace("+", "%2B");
203        r = r.replace("=", "%3D");
204        r = r.replace("?", "%3F");
205        r = r.replace("[", "%5B");
206        r = r.replace("]", "%5D");
207        if (atSequence != null) {
208            r = r.replace("@", atSequence);
209        }
210        if (slashSequence != null) {
211            r = r.replace("/", slashSequence);
212        }
213        return r;
214    }
215
216    public static String unquoteURIPathComponent(String s) {
217        if ("".equals(s)) {
218            return s;
219        }
220        URI uri;
221        try {
222            uri = new URI("http://x/" + s);
223        } catch (URISyntaxException e) {
224            throw new IllegalArgumentException("Illegal characters in: " + s, e);
225        }
226        String path = uri.getPath();
227        if (path.startsWith("/") && !s.startsWith("/")) {
228            path = path.substring(1);
229        }
230        return path;
231    }
232
233}