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.IOException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.nio.file.Files;
025import java.nio.file.Paths;
026import java.text.ParseException;
027import java.util.Optional;
028
029import javax.activation.MimetypesFileTypeMap;
030
031import static org.jdrupes.httpcodec.protocols.http.HttpConstants.*;
032import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus;
033import org.jdrupes.httpcodec.types.Converters;
034import org.jdrupes.httpcodec.types.MediaType;
035
036/**
037 * Represents an HTTP response header.
038 */
039public class HttpResponse extends HttpMessageHeader {
040
041        private static MimetypesFileTypeMap typesMap = new MimetypesFileTypeMap();
042        
043        private int statusCode = -1;
044        private String reasonPhrase;
045        private HttpRequest request;
046        
047        public HttpResponse(HttpProtocol protocol, HttpStatus status, 
048                        boolean hasPayload) {
049                super(protocol, hasPayload);
050                setStatus(status);
051        }
052        
053        public HttpResponse(HttpProtocol protocol, int statusCode, 
054                        String reasonPhrase, boolean hasPayload) {
055                super(protocol, hasPayload);
056                setStatusCode(statusCode);
057                setReasonPhrase(reasonPhrase);
058        }
059        
060        /* (non-Javadoc)
061         * @see HttpMessageHeader#setField(org.jdrupes.httpcodec.fields.HttpField)
062         */
063        @Override
064        public HttpResponse setField(HttpField<?> value) {
065                super.setField(value);
066                return this;
067        }
068        
069        /* (non-Javadoc)
070         * @see HttpMessageHeader#setField(java.lang.String, java.lang.Object)
071         */
072        @Override
073        public <T> HttpResponse setField(String name, T value) {
074                super.setField(name, value);
075                return this;
076        }
077
078        /* (non-Javadoc)
079         * @see org.jdrupes.httpcodec.protocols.http.HttpMessageHeader#setHasPayload(boolean)
080         */
081        @Override
082        public HttpResponse setHasPayload(boolean hasPayload) {
083                super.setHasPayload(hasPayload);
084                return this;
085        }
086
087        /**
088         * @return the responseCode
089         */
090        public int statusCode() {
091                return statusCode;
092        }
093
094        /**
095         * @param statusCode the responseCode to set
096         * @return the response for easy chaining
097         */
098        public HttpResponse setStatusCode(int statusCode) {
099                this.statusCode = statusCode;
100                return this;
101        }
102
103        /**
104         * @return the reason phrase
105         */
106        public String reasonPhrase() {
107                return reasonPhrase;
108        }
109
110        /**
111         * @param reasonPhrase the reason phrase to set
112         * @return the response for easy chaining
113         */
114        public HttpResponse setReasonPhrase(String reasonPhrase) {
115                this.reasonPhrase = reasonPhrase;
116                return this;
117        }
118
119        /**
120         * Sets both status code and reason phrase from the given 
121         * http status value.
122         * 
123         * @param status the status value
124         * @return the response for easy chaining
125         */
126        public HttpResponse setStatus(HttpStatus status) {
127                statusCode = status.statusCode();
128                reasonPhrase = status.reasonPhrase();
129                return this;
130        }
131        
132        /**
133         * Convenience method for setting the "Content-Type" header using 
134         * the given media type. Also sets the "has payload" flag.
135         * 
136         * @param mediaType the media type
137         * @return the response for easy chaining
138         */
139        public HttpResponse setContentType(MediaType mediaType) {
140                setField(HttpField.CONTENT_TYPE, mediaType);
141                setHasPayload(true);
142                return this;
143        }
144        
145        /**
146         * Convenience method for setting the "Content-Type" header
147         * from the given values. Also sets the "has payload" flag.
148         * 
149         * @param type the type
150         * @param subtype the subtype
151         * @return the response for easy chaining
152         * @throws ParseException if the values cannot be parsed
153         */
154        public HttpResponse setContentType(String type, String subtype) 
155                        throws ParseException {
156                return setContentType(new MediaType(type, subtype));
157        }
158
159        /**
160         * A convenience method for setting the "Content-Type" header (usually
161         * of type "text") together with its charset parameter. Also sets 
162         * the "has payload" flag.
163         * 
164         * @param type the type
165         * @param subtype the subtype
166         * @param charset the charset
167         * @return the response for easy chaining
168         * @throws ParseException if the values cannot be parsed
169         */
170        public HttpResponse setContentType(String type, String subtype,
171                        String charset) throws ParseException {
172                return setContentType(MediaType.builder().setType(type, subtype)
173                                .setParameter("charset", charset).build());
174        }
175        
176        /**
177         * Convenience method for setting the "Content-Type" header using 
178         * the path information of the given request. Also sets 
179         * the "has payload" flag.
180         * 
181         * @param requestUri the requested resource
182         * @return the response for easy chaining
183         */
184        public HttpResponse setContentType(URI requestUri) {
185                MediaType mediaType = contentType(requestUri);
186                setField(HttpField.CONTENT_TYPE, mediaType);
187                setHasPayload(true);
188                return this;
189        }
190        
191        /**
192         * Derives a media type from the given URI.
193         * 
194         * @param requestUri the uri
195         * @return the media type
196         */
197        public static MediaType contentType(URI requestUri) {
198                MediaType mediaType = new MediaType("application", "octet-stream");
199                while (requestUri.isOpaque()) {
200                        // Maybe the scheme specific part is a "nested" URI...
201                        try {
202                                requestUri = new URI(requestUri.getSchemeSpecificPart());
203                        } catch (URISyntaxException | NullPointerException e) {
204                                return mediaType;
205                        }
206                }
207                if (requestUri.getPath() == null) {
208                        return mediaType;
209                }
210                String mimeTypeName = null;
211                try {
212                        // probeContentType is most advanced, but may fail if it tries
213                        // to look at the file's content (which doesn't exist).
214                        mimeTypeName = Files.probeContentType(Paths.get(
215                                        requestUri.getPath()));
216                } catch (IOException e) {
217                        // Fall Through
218                }
219                if (mimeTypeName == null) {
220                        mimeTypeName = typesMap.getContentType(requestUri.getPath());
221                }
222                try {
223                        mediaType = Converters.MEDIA_TYPE.fromFieldValue(mimeTypeName);
224                } catch (ParseException e) {
225                        // Cannot happen
226                }
227                if ("text".equals(mediaType.topLevelType())) {
228                        mediaType = MediaType.builder().from(mediaType)
229                                        .setParameter("charset", System.getProperty(
230                                                        "file.encoding", "UTF-8")).build();
231                }
232                return mediaType;
233        }
234        
235        /**
236         * A convenience method for setting the "Content-Length" header.
237         * 
238         * @param length the length
239         * @return the response for easy chaining
240         */
241        public HttpResponse setContentLength(long length) {
242                return setField(new HttpField<>(
243                                HttpField.CONTENT_LENGTH, length, Converters.LONG));
244        }
245        
246        /**
247         * Associates the response with the request that it responds to. This method
248         * is invoked by the request decoder when it creates the prepared
249         * response for a request. The relationship with the request is required
250         * because information from the request headers may be needed when encoding
251         * the response. 
252         * 
253         * @param request
254         *            the request
255         * @return the response for easy chaining
256         * @see HttpRequest#setResponse(HttpResponse)
257         */
258        public HttpResponse setRequest(HttpRequest request) {
259                this.request = request;
260                return this;
261        }
262        
263        /**
264         * Returns the request that this response responds to.
265         * 
266         * @return the request
267         * @see #setRequest(HttpRequest)
268         */
269        public Optional<HttpRequest> request() {
270                return Optional.ofNullable(request);
271        }
272
273}