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