001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023
002package io.cui.tools.net;
003
004import static io.cui.tools.collect.CollectionLiterals.immutableList;
005import static io.cui.tools.string.MoreStrings.requireNotEmptyTrimmed;
006import static java.net.URLEncoder.encode;
007import static java.util.Objects.requireNonNull;
008import java.io.Serializable;
009import java.net.URLDecoder;
010import java.net.URLEncoder;
011import java.nio.charset.StandardCharsets;
012import java.util.ArrayList;
013import java.util.Collections;
014import java.util.Comparator;
015import java.util.HashMap;
016import java.util.List;
017import java.util.Map;
018import java.util.Map.Entry;
019import io.cui.tools.collect.CollectionBuilder;
020import io.cui.tools.collect.MoreCollections;
021import io.cui.tools.logging.CuiLogger;
022import io.cui.tools.string.Joiner;
023import io.cui.tools.string.MoreStrings;
024import io.cui.tools.string.Splitter;
025
026/**
027 * Simple wrapper around an Url Parameter Object.
028 * <p>
029 * Depending on the constructor arguments the attributes #getName() and
030 * #getValue() are implicitly encoded properly using
031 * {@link URLEncoder#encode(String, String)}. This is helpful for reliable
032 * handling of special characters.
033 * </p>
034 *
035 * @author Oliver Wolff
036 */
037public class UrlParameter implements Serializable, Comparable<UrlParameter> {
038    private static final CuiLogger log = new CuiLogger(UrlParameter.class);
039    /**
040     * Shortcut constant for faces redirect parameter.
041     */
042    public static final UrlParameter FACES_REDIRECT = new UrlParameter("faces-redirect", "true");
043    /**
044     * Shortcut constant parameter for includeViewParams.
045     */
046    public static final UrlParameter INCLUDE_VIEW_PARAMETER = new UrlParameter("includeViewParams", "true");
047    private static final long serialVersionUID = 634175928228707534L;
048    /**
049     * The name of the parameter.
050     */
051    private final String name;
052    /**
053     * The value of the parameter.
054     */
055    private final String value;
056
057    /**
058     * Constructor. Name and value are implicitly encoded using UTF-8.
059     *
060     * @param name
061     *            must not be null or empty
062     * @param value
063     *            may be null.
064     */
065    public UrlParameter(final String name, final String value) {
066        this(name, value, true);
067    }
068
069    /**
070     * Constructor.
071     *
072     * @param name
073     *            must not be null or empty
074     * @param value
075     *            value may be null.
076     * @param encode
077     *            indicates whether to encode the parameter name and value as
078     *            UTF-8
079     */
080    public UrlParameter(final String name, final String value, final boolean encode) {
081        super();
082        requireNotEmptyTrimmed(name, "Parameter name must not be empty");
083        if (encode) {
084            this.name = encode(name, StandardCharsets.UTF_8);
085            if (MoreStrings.isEmpty(value)) {
086                this.value = null;
087            } else {
088                this.value = encode(value, StandardCharsets.UTF_8);
089            }
090        } else {
091            this.name = name;
092            this.value = value;
093        }
094    }
095
096    /**
097     * Returns a boolean indicating whether the {@link UrlParameter} is empty,
098     * saying has a null value
099     *
100     * @return boolean flag whether the {@link UrlParameter} is empty
101     */
102    public boolean isEmpty() {
103        return null == value;
104    }
105
106    /**
107     * Creates a parameter String for a given number of {@link UrlParameter}.
108     *
109     * @param parameters
110     *            to be appended, must not be null
111     * @return the concatenated ParameterString in the form
112     *         "?parameter1Name=parameter1Value&amp;parameter2Name=parameter2Value"
113     */
114    public static String createParameterString(final UrlParameter... parameters) {
115        return createParameterString(false, parameters);
116    }
117
118    /**
119     * Create a String-representation of the URL-Parameter
120     *
121     * @param encode
122     * @param parameters
123     * @return the created parameter String
124     */
125    public static String createParameterString(final boolean encode, final UrlParameter... parameters) {
126        final var builder = new StringBuilder();
127        // First parameter to be treated specially.
128        if (null != parameters && parameters.length > 0 && null != parameters[0]) {
129            builder.append('?').append(parameters[0].createNameValueString(encode));
130            if (parameters.length > 1) {
131                // The other parameters are appended with '&'
132                for (var i = 1; i < parameters.length; i++) {
133                    builder.append('&').append(parameters[i].createNameValueString(encode));
134                }
135            }
136        }
137        return builder.toString();
138    }
139
140    // now need to throw exception
141    /**
142     * Convert a map of raw Url-parameter into a list of {@link UrlParameter}
143     *
144     * @param map
145     *            containing the parameter extracted usually directly from
146     *            servlet request. From the String[] solely the first element
147     *            will be extracted. The others will be ignored.
148     * @param parameterFilter
149     *            defines the parameter to be filtered. May be null or empty.
150     * @param encode
151     *            indicates whether to encode the parameter name and value as
152     *            UTF-8
153     * @return the found List of {@link UrlParameter} or empty list if the given
154     *         map is null or empty. The List is always sorted by #getName()
155     */
156    @SuppressWarnings("squid:S1166")
157    public static final List<UrlParameter> getUrlParameterFromMap(final Map<String, List<String>> map, final ParameterFilter parameterFilter, final boolean encode) {
158        if (MoreCollections.isEmpty(map)) {
159            return Collections.emptyList();
160        }
161        final List<UrlParameter> extracted = new ArrayList<>();
162        for (final Entry<String, List<String>> entry : map.entrySet()) {
163            String value = null;
164            if (!MoreCollections.isEmpty(entry.getValue())) {
165                value = entry.getValue().get(0);
166            }
167            final var key = entry.getKey();
168            if (null == parameterFilter || !parameterFilter.isExcluded(key)) {
169                try {
170                    extracted.add(new UrlParameter(key, value, encode));
171                } catch (final IllegalArgumentException e) {
172                    log.debug("Unable to read url parameter due to missing parameter name", e.getMessage());
173                }
174            }
175        }
176        extracted.sort(Comparator.comparing(UrlParameter::getName));
177        return extracted;
178    }
179
180    /**
181     * Filters the given list of {@link UrlParameter}
182     *
183     * @param toBeFiltered
184     *            may be null or empty
185     * @param parameterFilter
186     *            used for filtering, may be null
187     * @return the filtered parameter list or empty List if toBeFiltered is null
188     *         or empty.
189     */
190    public static List<UrlParameter> filterParameter(final List<UrlParameter> toBeFiltered, final ParameterFilter parameterFilter) {
191        if (toBeFiltered == null || toBeFiltered.isEmpty()) {
192            return Collections.emptyList();
193        }
194        final var filtered = new ArrayList<UrlParameter>();
195        for (final UrlParameter parameter : toBeFiltered) {
196            final var key = parameter.getName();
197            if (null == parameterFilter || !parameterFilter.isExcluded(key)) {
198                filtered.add(parameter);
199            }
200        }
201        return filtered;
202    }
203
204    /**
205     * Create a parameterMap for a given list of {@link UrlParameter}
206     *
207     * @param urlParameters
208     *            may be null or empty
209     * @return parameter Map, may be empty if urlParameters is empty. The list
210     *         of String will solely contain one element.
211     */
212    public static final Map<String, List<String>> createParameterMap(final List<UrlParameter> urlParameters) {
213        final Map<String, List<String>> result = new HashMap<>();
214        if (null != urlParameters && !urlParameters.isEmpty()) {
215            for (final UrlParameter urlParameter : urlParameters) {
216                result.put(urlParameter.getName(), immutableList(urlParameter.getValue()));
217            }
218        }
219        return result;
220    }
221
222    /**
223     * Helper class that create a list of {@link UrlParameter} from a given query-String
224     *
225     * @param queryString if it is null or empty or solely consists of an "?" an empty {@link List}
226     * @return if queryString is null or empty or solely consists of an "?" an empty {@link List}
227     *         will be returned. An immutable {@link List} of {@link UrlParameter} otherwise
228     */
229    public static List<UrlParameter> fromQueryString(String queryString) {
230        log.trace("Parsing Query String %s", queryString);
231        if (MoreStrings.isEmpty(queryString)) {
232            return Collections.emptyList();
233        }
234        var cleaned = queryString.trim();
235        if (cleaned.startsWith("?")) {
236            cleaned = cleaned.substring(1);
237        }
238        if (MoreStrings.isEmpty(cleaned)) {
239            log.debug("Given String solely consists of \'?\' symbol, ignoring");
240            return Collections.emptyList();
241        }
242        var elements = Splitter.on("&").omitEmptyStrings().splitToList(cleaned);
243        var builder = new CollectionBuilder<UrlParameter>();
244        for (String element : elements) {
245            if (element.contains("=")) {
246                var splitted = Splitter.on("=").omitEmptyStrings().splitToList(element);
247                switch (splitted.size()) {
248                case 0: 
249                    log.debug("Unable to parse queryString \'%s\' correctly, unable to extract key-value-pair for element \'%s\'", queryString, element);
250                    break;
251                case 1: 
252                    builder.add(createDecoded(splitted.get(0), null));
253                    break;
254                case 2: 
255                    builder.add(createDecoded(splitted.get(0), splitted.get(1)));
256                    break;
257                default: 
258                    log.debug("Unable to parse queryString \'%s\' correctly, multiple \'=\' symbols found at unexpected locations", queryString);
259                    break;
260                }
261            } else {
262                builder.add(createDecoded(element, null));
263            }
264        }
265        return builder.toImmutableList();
266    }
267
268    private static UrlParameter createDecoded(final String name, final String value) {
269        requireNonNull(name);
270        String decodedKey;
271        decodedKey = URLDecoder.decode(name, StandardCharsets.UTF_8);
272        String decodedValue = null;
273        if (null != value) {
274            decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
275        }
276        return new UrlParameter(decodedKey, decodedValue, false);
277    }
278
279    /**
280     * Create a String representation of a name value pair, saying name=value
281     *
282     * @return String representation of a name value pair, saying name=value
283     */
284    public String createNameValueString() {
285        return createNameValueString(false);
286    }
287
288    /**
289     * @param encode
290     *            flag indicate if the result need to be encoded
291     * @return string representation of name + vale
292     */
293    public String createNameValueString(final boolean encode) {
294        if (encode) {
295            return new UrlParameter(name, value).createNameValueString();
296        }
297        return Joiner.on('=').useForNull("").join(name, value);
298    }
299
300    @Override
301    public int compareTo(final UrlParameter compareTo) {
302        return getName().compareTo(compareTo.getName());
303    }
304
305    @java.lang.Override
306    @java.lang.SuppressWarnings("all")
307    @lombok.Generated
308    public boolean equals(final java.lang.Object o) {
309        if (o == this) return true;
310        if (!(o instanceof UrlParameter)) return false;
311        final UrlParameter other = (UrlParameter) o;
312        if (!other.canEqual((java.lang.Object) this)) return false;
313        final java.lang.Object this$name = this.getName();
314        final java.lang.Object other$name = other.getName();
315        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
316        final java.lang.Object this$value = this.getValue();
317        final java.lang.Object other$value = other.getValue();
318        if (this$value == null ? other$value != null : !this$value.equals(other$value)) return false;
319        return true;
320    }
321
322    @java.lang.SuppressWarnings("all")
323    @lombok.Generated
324    protected boolean canEqual(final java.lang.Object other) {
325        return other instanceof UrlParameter;
326    }
327
328    @java.lang.Override
329    @java.lang.SuppressWarnings("all")
330    @lombok.Generated
331    public int hashCode() {
332        final int PRIME = 59;
333        int result = 1;
334        final java.lang.Object $name = this.getName();
335        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
336        final java.lang.Object $value = this.getValue();
337        result = result * PRIME + ($value == null ? 43 : $value.hashCode());
338        return result;
339    }
340
341    @java.lang.Override
342    @java.lang.SuppressWarnings("all")
343    @lombok.Generated
344    public java.lang.String toString() {
345        return "UrlParameter(name=" + this.getName() + ", value=" + this.getValue() + ")";
346    }
347
348    /**
349     * The name of the parameter.
350     */
351    @java.lang.SuppressWarnings("all")
352    @lombok.Generated
353    public String getName() {
354        return this.name;
355    }
356
357    /**
358     * The value of the parameter.
359     */
360    @java.lang.SuppressWarnings("all")
361    @lombok.Generated
362    public String getValue() {
363        return this.value;
364    }
365}