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