001/* 002 * This file is part of the JDrupes non-blocking HTTP Codec 003 * Copyright (C) 2016, 2017 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.URLDecoder; 024import java.nio.charset.Charset; 025import java.nio.charset.StandardCharsets; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Optional; 032import java.util.StringTokenizer; 033 034import static org.jdrupes.httpcodec.protocols.http.HttpConstants.*; 035 036/** 037 * Represents an HTTP request header. 038 */ 039public class HttpRequest extends HttpMessageHeader { 040 041 public static final URI ASTERISK_REQUEST 042 = URI.create("http://127.0.0.1/"); 043 044 private String method; 045 private URI requestUri; 046 private String host; 047 private int port; 048 private HttpResponse response; 049 private Map<String, List<String>> decodedQuery = null; 050 051 /** 052 * Creates a new request with basic data. The {@link #host()} 053 * and {@link #port()} values are initialized with the values from 054 * the `requestUri`. 055 * 056 * @param method the method 057 * @param requestUri the requested resource 058 * @param httpProtocol the HTTP protocol version 059 * @param hasPayload indicates that the message has a payload body 060 */ 061 public HttpRequest(String method, URI requestUri, 062 HttpProtocol httpProtocol, boolean hasPayload) { 063 super(httpProtocol, hasPayload); 064 this.method = method; 065 this.requestUri = requestUri; 066 this.host = requestUri.getHost(); 067 this.port = requestUri.getPort(); 068 } 069 070 /* 071 * (non-Javadoc) 072 * 073 * @see HttpMessageHeader#setField(org.jdrupes.httpcodec.fields.HttpField) 074 */ 075 @Override 076 public HttpRequest setField(HttpField<?> value) { 077 super.setField(value); 078 return this; 079 } 080 081 /* 082 * (non-Javadoc) 083 * 084 * @see HttpMessageHeader#setField(java.lang.String, java.lang.Object) 085 */ 086 @Override 087 public <T> HttpRequest setField(String name, T value) { 088 super.setField(name, value); 089 return this; 090 } 091 092 /* 093 * (non-Javadoc) 094 * 095 * @see 096 * org.jdrupes.httpcodec.protocols.http.HttpMessageHeader#setHasPayload( 097 * boolean) 098 */ 099 @Override 100 public HttpRequest setHasPayload(boolean hasPayload) { 101 super.setHasPayload(hasPayload); 102 return this; 103 } 104 105 /** 106 * Return the method. 107 * 108 * @return the method 109 */ 110 public String method() { 111 return method; 112 } 113 114 /** 115 * Return the URI of the requested resource. 116 * 117 * @return the requestUri 118 */ 119 public URI requestUri() { 120 return requestUri; 121 } 122 123 /** 124 * Set the host and port attributes. 125 * 126 * @param host the host 127 * @param port the port 128 * @return the request for easy chaining 129 */ 130 public HttpRequest setHostAndPort(String host, int port) { 131 this.host = host; 132 this.port = port; 133 return this; 134 } 135 136 /** 137 * @return the host 138 */ 139 public String host() { 140 return host; 141 } 142 143 /** 144 * @return the port 145 */ 146 public int port() { 147 return port; 148 } 149 150 /** 151 * Associates the request with a response. This method is 152 * invoked by the request decoder that initializes the response with 153 * basic information that can be derived from the request 154 * (e.g. by default the HTTP version is copied). The status code 155 * of the preliminary response is 501 "Not implemented". 156 * <P> 157 * Although not strictly required, users of the API are encouraged to 158 * modify this prepared request and use it when building the response. 159 * 160 * @param response the prepared response 161 * @return the request for easy chaining 162 */ 163 public HttpRequest setResponse(HttpResponse response) { 164 this.response = response; 165 return this; 166 } 167 168 /** 169 * Returns the prepared response. 170 * 171 * @return the prepared response 172 * @see #setResponse(HttpResponse) 173 */ 174 public Optional<HttpResponse> response() { 175 return Optional.ofNullable(response); 176 } 177 178 /** 179 * Returns the decoded query data from the request URI. The result 180 * is a lazily created (and cached) unmodifiable map. 181 * 182 * @param charset the charset to use for decoding 183 * @return the data 184 * @throws UnsupportedEncodingException the unsupported encoding exception 185 */ 186 public Map<String, List<String>> queryData(Charset charset) 187 throws UnsupportedEncodingException { 188 if (decodedQuery != null) { 189 return decodedQuery; 190 } 191 if (requestUri.getRawQuery() == null 192 || requestUri.getRawQuery().length() == 0) { 193 decodedQuery = Collections.emptyMap(); 194 return decodedQuery; 195 } 196 Map<String, List<String>> queryData = new HashMap<>(); 197 StringTokenizer pairStrings 198 = new StringTokenizer(requestUri.getRawQuery(), "&"); 199 while (pairStrings.hasMoreTokens()) { 200 StringTokenizer pair 201 = new StringTokenizer(pairStrings.nextToken(), "="); 202 String key = URLDecoder.decode(pair.nextToken(), charset.name()); 203 String value = pair.hasMoreTokens() 204 ? URLDecoder.decode(pair.nextToken(), charset.name()) 205 : null; 206 queryData.computeIfAbsent(key, k -> new ArrayList<>()).add(value); 207 } 208 for (Map.Entry<String, List<String>> entry : queryData.entrySet()) { 209 entry.setValue(Collections.unmodifiableList(entry.getValue())); 210 } 211 decodedQuery = Collections.unmodifiableMap(queryData); 212 return decodedQuery; 213 } 214 215 /** 216 * Short for invoking {@link #queryData(Charset)} with UTF-8 as charset. 217 * 218 * @return the map 219 */ 220 public Map<String, List<String>> queryData() { 221 try { 222 return queryData(StandardCharsets.UTF_8); 223 } catch (UnsupportedEncodingException e) { 224 // Cannot happen 225 throw new IllegalStateException(e); 226 } 227 } 228 229 /* 230 * (non-Javadoc) 231 * 232 * @see java.lang.Object#toString() 233 */ 234 @Override 235 public String toString() { 236 StringBuilder builder = new StringBuilder(); 237 builder.append("HttpRequest ["); 238 if (method != null) { 239 builder.append("method="); 240 builder.append(method); 241 builder.append(", "); 242 } 243 if (requestUri != null) { 244 builder.append("requestUri="); 245 builder.append(requestUri); 246 builder.append(", "); 247 } 248 if (protocol() != null) { 249 builder.append("httpVersion="); 250 builder.append(protocol()); 251 } 252 builder.append("]"); 253 return builder.toString(); 254 } 255 256}