001/*
002 * Copyright (c) 2010-2024 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;
028import static java.util.stream.Collectors.toList;
029
030import java.io.UnsupportedEncodingException;
031import java.nio.charset.StandardCharsets;
032import java.util.Arrays;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.regex.Pattern;
037import java.util.stream.Collectors;
038
039/**
040 * @author <a href="http://restfb.com">Mark Allen</a>
041 * @since 1.6.10
042 */
043public final class UrlUtils {
044
045  /**
046   * Prevents instantiation.
047   */
048  private UrlUtils() {
049    throw new IllegalStateException("UrlUtils must not be instantiated");
050  }
051
052  /**
053   * URL-encodes a string.
054   * <p>
055   * Assumes {@code string} is in {@link StandardCharsets#UTF_8} format.
056   * 
057   * @param string
058   *          The string to URL-encode.
059   * @return The URL-encoded version of the input string, or {@code null} if {@code string} is {@code null}.
060   * @throws IllegalStateException
061   *           If unable to URL-encode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
062   */
063  public static String urlEncode(String string) {
064    if (string == null) {
065      return null;
066    }
067    try {
068      return encode(string, StandardCharsets.UTF_8.name());
069    } catch (UnsupportedEncodingException e) {
070      throw new IllegalStateException("Platform doesn't support " + StandardCharsets.UTF_8.name(), e);
071    }
072  }
073
074  /**
075   * URL-decodes a string.
076   * <p>
077   * Assumes {@code string} is in {@link StandardCharsets#UTF_8} format.
078   * 
079   * @param string
080   *          The string to URL-decode.
081   * @return The URL-decoded version of the input string, or {@code null} if {@code string} is {@code null}.
082   * @throws IllegalStateException
083   *           If unable to URL-decode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
084   * @since 1.6.5
085   */
086  public static String urlDecode(String string) {
087    if (string == null) {
088      return null;
089    }
090    try {
091      return decode(string, StandardCharsets.UTF_8.name());
092    } catch (UnsupportedEncodingException e) {
093      throw new IllegalStateException("Platform doesn't support " + StandardCharsets.UTF_8.name(), e);
094    }
095  }
096
097  /**
098   * For the given {@code queryString}, extract a mapping of query string parameter names to values.
099   * <p>
100   * Example of a {@code queryString} is {@code accessToken=123&expires=345}.
101   * 
102   * @param queryString
103   *          The URL query string from which parameters are extracted.
104   * @return A mapping of query string parameter names to values. If {@code queryString} is {@code null}, an empty
105   *         {@code Map} is returned.
106   * @throws IllegalStateException
107   *           If unable to URL-decode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
108   */
109  public static Map<String, List<String>> extractParametersFromQueryString(String queryString) {
110    if (queryString == null) {
111      return emptyMap();
112    }
113
114    // If there is no ? character at the front of the string, append it.
115    return extractParametersFromUrl(
116      format("restfb://url%s", queryString.startsWith("?") ? queryString : "?" + queryString));
117  }
118
119  /**
120   * For the given {@code url}, extract a mapping of query string parameter names to values.
121   * <p>
122   * Adapted from an implementation by BalusC and dfrankow, available at
123   * http://stackoverflow.com/questions/1667278/parsing-query-strings-in-java.
124   * 
125   * @param url
126   *          The URL from which parameters are extracted.
127   * @return A mapping of query string parameter names to values. If {@code url} is {@code null}, an empty {@code Map}
128   *         is returned.
129   * @throws IllegalStateException
130   *           If unable to URL-decode because the JVM doesn't support {@link StandardCharsets#UTF_8}.
131   */
132  public static Map<String, List<String>> extractParametersFromUrl(String url) {
133    if (url == null) {
134      return emptyMap();
135    }
136
137    Map<String, List<String>> parameters = new HashMap<>();
138    String[] urlParts = url.split("\\?");
139
140    if (urlParts.length > 1) {
141      String query = urlParts[1];
142      parameters = Pattern.compile("&").splitAsStream(query) //
143        .map(s -> Arrays.copyOf(s.split("="), 2))
144        .collect(Collectors.groupingBy(s -> urlDecode(s[0]), Collectors.mapping(s -> urlDecode(s[1]), toList())));
145    }
146
147    return parameters;
148  }
149
150  /**
151   * Modify the query string in the given {@code url} and return the new url as String.
152   * <p>
153   * The given key/value pair is added to the url. If the key is already present, it is replaced with the new value.
154   *
155   * @param url
156   *          The URL which parameters should be modified.
157   * @param key
158   *          the key, that should be modified or added
159   * @param value
160   *          the value of the key/value pair
161   * @return the modified URL as String
162   */
163  public static String replaceOrAddQueryParameter(String url, String key, String value) {
164    String[] urlParts = url.split("\\?");
165    String qParameter = key + "=" + value;
166
167    if (urlParts.length == 2) {
168      Map<String, List<String>> paramMap = extractParametersFromQueryString(urlParts[1]);
169      if (paramMap.containsKey(key)) {
170        String queryValue = paramMap.get(key).get(0);
171        return url.replace(key + "=" + queryValue, qParameter);
172      } else {
173        return url + "&" + qParameter;
174      }
175
176    } else {
177      return url + "?" + qParameter;
178    }
179  }
180
181  /**
182   * Remove the given key from the url query string and return the new URL as String.
183   *
184   * @param url
185   *          The URL from which parameters are extracted.
186   * @param key
187   *          the key, that should be removed
188   * @return the modified URL as String
189   */
190  public static String removeQueryParameter(String url, String key) {
191    String[] urlParts = url.split("\\?");
192
193    if (urlParts.length == 2) {
194      Map<String, List<String>> paramMap = extractParametersFromQueryString(urlParts[1]);
195      if (paramMap.containsKey(key)) {
196        String queryValue = paramMap.get(key).get(0);
197        String result = url.replace(key + "=" + queryValue, "");
198        // improper separators have to be fixed
199        // @TODO find a better way to solve this
200        result = result.replace("?&", "?").replace("&&", "&");
201        if (result.endsWith("&")) {
202          return result.substring(0, result.length() - 1);
203        } else {
204          return result;
205        }
206      }
207    }
208    return url;
209  }
210}