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}