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