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}