001/* 002 * This file is part of the JDrupes non-blocking HTTP Codec 003 * Copyright (C) 2016, 2024 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Lesser General Public License as published 007 * by the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 013 * License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.httpcodec.protocols.http; 020 021import java.io.UnsupportedEncodingException; 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.net.URLDecoder; 025import java.net.URLEncoder; 026import java.nio.charset.Charset; 027import java.nio.charset.StandardCharsets; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Optional; 034import java.util.StringTokenizer; 035import java.util.function.Function; 036 037import static org.jdrupes.httpcodec.protocols.http.HttpConstants.*; 038 039/** 040 * Represents an HTTP request header. 041 */ 042public class HttpRequest extends HttpMessageHeader { 043 044 /** The Constant ASTERISK_REQUEST. */ 045 public static final URI ASTERISK_REQUEST 046 = URI.create("http://127.0.0.1/"); 047 048 private String method; 049 private URI requestUri; 050 private String host; 051 private int port; 052 private HttpResponse response; 053 private Map<String, List<String>> decodedQuery = null; 054 055 /** 056 * Creates a new request with basic data. The {@link #host()} 057 * and {@link #port()} values are initialized with the values from 058 * the `requestUri`. 059 * 060 * @param method the method 061 * @param requestUri the requested resource 062 * @param httpProtocol the HTTP protocol version 063 * @param hasPayload indicates that the message has a payload body 064 */ 065 public HttpRequest(String method, URI requestUri, 066 HttpProtocol httpProtocol, boolean hasPayload) { 067 super(httpProtocol, hasPayload); 068 this.method = method; 069 this.requestUri = requestUri; 070 this.host = requestUri.getHost(); 071 this.port = requestUri.getPort(); 072 } 073 074 /* 075 * (non-Javadoc) 076 * 077 * @see HttpMessageHeader#setField(org.jdrupes.httpcodec.fields.HttpField) 078 */ 079 @Override 080 public HttpRequest setField(HttpField<?> value) { 081 super.setField(value); 082 return this; 083 } 084 085 /* 086 * (non-Javadoc) 087 * 088 * @see HttpMessageHeader#setField(java.lang.String, java.lang.Object) 089 */ 090 @Override 091 public <T> HttpRequest setField(String name, T value) { 092 super.setField(name, value); 093 return this; 094 } 095 096 /* 097 * (non-Javadoc) 098 * 099 * @see 100 * org.jdrupes.httpcodec.protocols.http.HttpMessageHeader#setHasPayload( 101 * boolean) 102 */ 103 @Override 104 public HttpRequest setHasPayload(boolean hasPayload) { 105 super.setHasPayload(hasPayload); 106 return this; 107 } 108 109 /** 110 * Return the method. 111 * 112 * @return the method 113 */ 114 public String method() { 115 return method; 116 } 117 118 /** 119 * Return the URI of the requested resource. 120 * 121 * @return the requestUri 122 */ 123 public URI requestUri() { 124 return requestUri; 125 } 126 127 /** 128 * Set the host and port attributes. 129 * 130 * @param host the host 131 * @param port the port 132 * @return the request for easy chaining 133 */ 134 public HttpRequest setHostAndPort(String host, int port) { 135 this.host = host; 136 this.port = port; 137 return this; 138 } 139 140 /** 141 * Host. 142 * 143 * @return the host 144 */ 145 public String host() { 146 return host; 147 } 148 149 /** 150 * Port. 151 * 152 * @return the port 153 */ 154 public int port() { 155 return port; 156 } 157 158 /** 159 * Associates the request with a response. This method is 160 * invoked by the request decoder that initializes the response with 161 * basic information that can be derived from the request 162 * (e.g. by default the HTTP version is copied). The status code 163 * of the preliminary response is 501 "Not implemented". 164 * <P> 165 * Although not strictly required, users of the API are encouraged to 166 * modify this prepared request and use it when building the response. 167 * 168 * @param response the prepared response 169 * @return the request for easy chaining 170 */ 171 public HttpRequest setResponse(HttpResponse response) { 172 this.response = response; 173 return this; 174 } 175 176 /** 177 * Returns the prepared response. 178 * 179 * @return the prepared response 180 * @see #setResponse(HttpResponse) 181 */ 182 public Optional<HttpResponse> response() { 183 return Optional.ofNullable(response); 184 } 185 186 /** 187 * Returns the decoded query data from the request URI. The result 188 * is a lazily created (and cached) unmodifiable map. 189 * 190 * @param charset the charset to use for decoding 191 * @return the data 192 * @throws UnsupportedEncodingException the unsupported encoding exception 193 */ 194 public Map<String, List<String>> queryData(Charset charset) 195 throws UnsupportedEncodingException { 196 if (decodedQuery != null) { 197 return decodedQuery; 198 } 199 if (requestUri.getRawQuery() == null 200 || requestUri.getRawQuery().length() == 0) { 201 decodedQuery = Collections.emptyMap(); 202 return decodedQuery; 203 } 204 Map<String, List<String>> queryData = new HashMap<>(); 205 StringTokenizer pairStrings 206 = new StringTokenizer(requestUri.getRawQuery(), "&"); 207 while (pairStrings.hasMoreTokens()) { 208 StringTokenizer pair 209 = new StringTokenizer(pairStrings.nextToken(), "="); 210 String key = URLDecoder.decode(pair.nextToken(), charset.name()); 211 String value = pair.hasMoreTokens() 212 ? URLDecoder.decode(pair.nextToken(), charset.name()) 213 : null; 214 queryData.computeIfAbsent(key, k -> new ArrayList<>()).add(value); 215 } 216 for (Map.Entry<String, List<String>> entry : queryData.entrySet()) { 217 entry.setValue(Collections.unmodifiableList(entry.getValue())); 218 } 219 decodedQuery = Collections.unmodifiableMap(queryData); 220 return decodedQuery; 221 } 222 223 /** 224 * Short for invoking {@link #queryData(Charset)} with UTF-8 as charset. 225 * 226 * @return the map 227 */ 228 public Map<String, List<String>> queryData() { 229 try { 230 return queryData(StandardCharsets.UTF_8); 231 } catch (UnsupportedEncodingException e) { 232 // Cannot happen 233 throw new IllegalStateException(e); 234 } 235 } 236 237 /** 238 * Updates the query part of an URI. 239 * 240 * @param uri the uri 241 * @param query the query in raw form, i.e. as it should appear 242 * in the request 243 * @return the new URI 244 */ 245 public static URI replaceQuery(URI uri, String query) { 246 try { 247 // Replace query, working around JDK query encoding problem 248 return new URI(new URI(uri.getScheme(), 249 uri.getAuthority(), uri.getPath(), null, null).toString() 250 + (query.isBlank() ? "" : ("?" + query)) 251 + (uri.getRawFragment() != null 252 ? ("#" + uri.getRawFragment()) 253 : "")); 254 } catch (URISyntaxException e) { 255 // Cannot happen 256 return uri; 257 } 258 } 259 260 /** 261 * Updates the query part of the request URI. 262 * 263 * @param data the data 264 * @param charset the charset to use for encoding keys and values 265 * @return the http request 266 */ 267 public HttpRequest setSimpleQueryData(Map<String, String> data, 268 Charset charset) { 269 requestUri 270 = replaceQuery(requestUri, simpleWwwFormUrlencode(data, charset)); 271 return this; 272 } 273 274 /** 275 * Updates the query part of the request URI, using UTF-8 to encode 276 * the query keys and values. 277 * 278 * @param data the data 279 * @return the http request 280 */ 281 public HttpRequest setSimpleQueryData(Map<String, String> data) { 282 return setSimpleQueryData(data, StandardCharsets.UTF_8); 283 } 284 285 /** 286 * Updates the query part of the request URI. 287 * 288 * @param data the data 289 * @param charset the charset to use for encoding keys and values 290 * @return the http request 291 */ 292 public HttpRequest setQueryData(Map<String, List<String>> data, 293 Charset charset) { 294 requestUri = replaceQuery(requestUri, wwwFormUrlencode(data, charset)); 295 return this; 296 } 297 298 /** 299 * Updates the query part of the request URI, using UTF-8 to encode 300 * the query keys and values. 301 * 302 * @param data the data 303 * @return the http request 304 */ 305 public HttpRequest setQueryData(Map<String, List<String>> data) { 306 return setQueryData(data, StandardCharsets.UTF_8); 307 } 308 309 /* 310 * (non-Javadoc) 311 * 312 * @see java.lang.Object#toString() 313 */ 314 @Override 315 public String toString() { 316 StringBuilder builder = new StringBuilder(); 317 builder.append("HttpRequest ["); 318 if (method != null) { 319 builder.append("method="); 320 builder.append(method); 321 builder.append(", "); 322 } 323 if (requestUri != null) { 324 builder.append("requestUri="); 325 builder.append(requestUri); 326 builder.append(", "); 327 } 328 if (protocol() != null) { 329 builder.append("httpVersion="); 330 builder.append(protocol()); 331 } 332 builder.append("]"); 333 return builder.toString(); 334 } 335 336 /** 337 * Www-form-urlencodes the given data, using the given charset 338 * to encode keys and values. 339 * 340 * @param data the data 341 * @param charset the charset to use for encoding keys and values 342 * @return the string 343 */ 344 public static String simpleWwwFormUrlencode(Map<String, String> data, 345 Charset charset) { 346 return data.entrySet().stream() 347 .map(e -> URLEncoder.encode(e.getKey(), charset) 348 + "=" + URLEncoder.encode(e.getValue(), charset)) 349 .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); 350 } 351 352 /** 353 * Www-form-urlencodes the given data, using UTF-8 354 * to encode keys and values. 355 * 356 * @param data the data 357 * @param charset the charset 358 * @return the string 359 */ 360 public static String simpleWwwFormUrlencode(Map<String, String> data) { 361 return simpleWwwFormUrlencode(data, StandardCharsets.UTF_8); 362 } 363 364 /** 365 * Www-form-urlencodes the given data, using the given charset 366 * to encode keys and values. 367 * 368 * @param data the data 369 * @param charset the charset to use for encoding keys and values 370 * @return the string 371 */ 372 public static String wwwFormUrlencode(Map<String, List<String>> data, 373 Charset charset) { 374 return data.entrySet().stream() 375 .map(e -> e.getValue().stream() 376 .map(v -> URLEncoder.encode(e.getKey(), charset) + "=" 377 + URLEncoder.encode(v, charset))) 378 .flatMap(Function.identity()) 379 .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); 380 } 381 382 /** 383 * Www-form-urlencodes the given data, using UTF-8 384 * to encode keys and values. 385 * 386 * @param data the data 387 * @param charset the charset 388 * @return the string 389 */ 390 public static String wwwFormUrlencode(Map<String, List<String>> data) { 391 return wwwFormUrlencode(data, StandardCharsets.UTF_8); 392 } 393 394}