001/**
002 * Copyright (c) 2010-2019 Mark Allen, Norbert Bartels.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.restfb.util;
023
024import static java.lang.String.format;
025import static java.net.URLDecoder.decode;
026import static java.net.URLEncoder.encode;
027import static java.util.Collections.emptyMap;
028
029import java.io.UnsupportedEncodingException;
030import java.nio.charset.StandardCharsets;
031import java.util.ArrayList;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036/**
037 * @author <a href="http://restfb.com">Mark Allen</a>
038 * @since 1.6.10
039 */
040public final class UrlUtils {
041
042  /**
043   * Prevents instantiation.
044   */
045  private UrlUtils() {
046    // Prevents instantiation
047  }
048
049  /**
050   * URL-encodes a string.
051   * <p>
052   * Assumes {@code string} is in {@link StandardCharsets#UTF_8} format.
053   * 
054   * @param string
055   *          The string to URL-encode.
056   * @return The URL-encoded version of the input string, or {@code null} if {@code string} is {@code null}.
057   * @throws IllegalStateException
058   *           If unable to URL-encode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
059   */
060  public static String urlEncode(String string) {
061    if (string == null) {
062      return null;
063    }
064    try {
065      return encode(string, StandardCharsets.UTF_8.name());
066    } catch (UnsupportedEncodingException e) {
067      throw new IllegalStateException("Platform doesn't support " + StandardCharsets.UTF_8.name(), e);
068    }
069  }
070
071  /**
072   * URL-decodes a string.
073   * <p>
074   * Assumes {@code string} is in {@link StandardCharsets#UTF_8} format.
075   * 
076   * @param string
077   *          The string to URL-decode.
078   * @return The URL-decoded version of the input string, or {@code null} if {@code string} is {@code null}.
079   * @throws IllegalStateException
080   *           If unable to URL-decode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
081   * @since 1.6.5
082   */
083  public static String urlDecode(String string) {
084    if (string == null) {
085      return null;
086    }
087    try {
088      return decode(string, StandardCharsets.UTF_8.name());
089    } catch (UnsupportedEncodingException e) {
090      throw new IllegalStateException("Platform doesn't support " + StandardCharsets.UTF_8.name(), e);
091    }
092  }
093
094  /**
095   * For the given {@code queryString}, extract a mapping of query string parameter names to values.
096   * <p>
097   * Example of a {@code queryString} is {@code accessToken=123&expires=345}.
098   * 
099   * @param queryString
100   *          The URL query string from which parameters are extracted.
101   * @return A mapping of query string parameter names to values. If {@code queryString} is {@code null}, an empty
102   *         {@code Map} is returned.
103   * @throws IllegalStateException
104   *           If unable to URL-decode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
105   */
106  public static Map<String, List<String>> extractParametersFromQueryString(String queryString) {
107    if (queryString == null) {
108      return emptyMap();
109    }
110
111    // If there is no ? character at the front of the string, append it.
112    return extractParametersFromUrl(
113      format("restfb://url%s", queryString.startsWith("?") ? queryString : "?" + queryString));
114  }
115
116  /**
117   * For the given {@code url}, extract a mapping of query string parameter names to values.
118   * <p>
119   * Adapted from an implementation by BalusC and dfrankow, available at
120   * http://stackoverflow.com/questions/1667278/parsing-query-strings-in-java.
121   * 
122   * @param url
123   *          The URL from which parameters are extracted.
124   * @return A mapping of query string parameter names to values. If {@code url} is {@code null}, an empty {@code Map}
125   *         is returned.
126   * @throws IllegalStateException
127   *           If unable to URL-decode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
128   */
129  public static Map<String, List<String>> extractParametersFromUrl(String url) {
130    if (url == null) {
131      return emptyMap();
132    }
133
134    Map<String, List<String>> parameters = new HashMap<>();
135
136    String[] urlParts = url.split("\\?");
137
138    if (urlParts.length > 1) {
139      String query = urlParts[1];
140
141      for (String param : query.split("&")) {
142        String[] pair = param.split("=");
143        String key = urlDecode(pair[0]);
144        String value = "";
145
146        if (pair.length > 1) {
147          value = urlDecode(pair[1]);
148        }
149
150        List<String> values = parameters.get(key);
151
152        if (values == null) {
153          values = new ArrayList<>();
154          parameters.put(key, values);
155        }
156
157        values.add(value);
158      }
159    }
160
161    return parameters;
162  }
163
164  /**
165   * Modify the query string in the given {@code url} and return the new url as String.
166   * <p>
167   * The given key/value pair is added to the url. If the key is already present, it is replaced with the new value.
168   *
169   * @param url
170   *          The URL which parameters should be modified.
171   * @param key
172   *          the key, that should be modified or added
173   * @param value
174   *          the value of the key/value pair
175   * @return the modified URL as String
176   */
177  public static String replaceOrAddQueryParameter(String url, String key, String value) {
178    String[] urlParts = url.split("\\?");
179    String qParameter = key + "=" + value;
180
181    if (urlParts.length == 2) {
182      Map<String, List<String>> paramMap = extractParametersFromQueryString(urlParts[1]);
183      if (paramMap.containsKey(key)) {
184        String queryValue = paramMap.get(key).get(0);
185        return url.replace(key + "=" + queryValue, qParameter);
186      } else {
187        return url + "&" + qParameter;
188      }
189
190    } else {
191      return url + "?" + qParameter;
192    }
193  }
194
195  /**
196   * Remove the given key from the url query string and return the new URL as String.
197   *
198   * @param url
199   *          The URL from which parameters are extracted.
200   * @param key
201   *          the key, that should be removed
202   * @return the modified URL as String
203   */
204  public static String removeQueryParameter(String url, String key) {
205    String[] urlParts = url.split("\\?");
206
207    if (urlParts.length == 2) {
208      Map<String, List<String>> paramMap = extractParametersFromQueryString(urlParts[1]);
209      if (paramMap.containsKey(key)) {
210        String queryValue = paramMap.get(key).get(0);
211        String result = url.replace(key + "=" + queryValue, "");
212        // improper separators have to be fixed
213        // @TODO find a better way to solve this
214        result = result.replace("?&", "?").replace("&&", "&");
215        if (result.endsWith("&")) {
216          return result.substring(0, result.length() - 1);
217        } else {
218          return result;
219        }
220      }
221    }
222    return url;
223  }
224}