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}