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.text.ParseException; 022import java.util.Collections; 023import java.util.Map; 024import java.util.Optional; 025import java.util.TreeMap; 026import java.util.function.Supplier; 027 028import org.jdrupes.httpcodec.MessageHeader; 029 030import static org.jdrupes.httpcodec.protocols.http.HttpConstants.*; 031 032import org.jdrupes.httpcodec.types.Converter; 033import org.jdrupes.httpcodec.types.Converters; 034import org.jdrupes.httpcodec.types.StringList; 035 036/** 037 * Represents an HTTP message header (either request or response). 038 * 039 * The main addition to the base classe {@link MessageHeader} is 040 * the handling of header fields. 041 * 042 * ![Classes](header.svg) 043 * 044 * @startuml header.svg 045 * abstract class HttpMessageHeader { 046 * +HttpProtocol protocol() 047 * +Map<String,HttpField<?>> fields() 048 * +HttpMessageHeader setField(HttpField<?> value) 049 * +HttpMessageHeader setField(String name, T value) 050 * +HttpMessageHeader clearHeaders() 051 * +HttpMessageHeader removeField(String name) 052 * +Optional<HttpField<T>> findField(String name, Converter<T> converter) 053 * +Optional<T> findValue(String name, Converter<T> converter) 054 * +Optional<String> findStringValue(String name) 055 * +HttpField<T> computeIfAbsent(String name, Converter<T> converter, Supplier<T> supplier) 056 * +MessageHeader setHasPayload(boolean hasPayload) 057 * +boolean hasPayload() 058 * +boolean isFinal() 059 * } 060 * class MessageHeader { 061 * } 062 * MessageHeader <|-- HttpMessageHeader 063 * 064 * class HttpField<T> { 065 * +String name() 066 * +T value() 067 * } 068 * 069 * HttpMessageHeader *-right- HttpField 070 * 071 * @enduml 072 */ 073public abstract class HttpMessageHeader implements MessageHeader { 074 075 private HttpProtocol httpProtocol; 076 private Map<String,HttpField<?>> headers 077 = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 078 private boolean hasPayload; 079 080 /** 081 * Creates a new message header. 082 * 083 * @param httpProtocol the HTTP protocol 084 * @param hasPayload indicates that a body is expected after the header 085 */ 086 public HttpMessageHeader(HttpProtocol httpProtocol, boolean hasPayload) { 087 this.httpProtocol = httpProtocol; 088 this.hasPayload = hasPayload; 089 } 090 091 /** 092 * Return the protocol. 093 * 094 * @return the HTTP protocol 095 */ 096 public HttpProtocol protocol() { 097 return httpProtocol; 098 } 099 100 /** 101 * Returns all header fields as unmodifiable map. 102 * 103 * @return the headers 104 */ 105 public Map<String, HttpField<?>> fields() { 106 return Collections.unmodifiableMap(headers); 107 } 108 109 /** 110 * Sets a header field for the message. 111 * 112 * @param value the header field's value 113 * @return the message header for easy chaining 114 */ 115 public HttpMessageHeader setField(HttpField<?> value) { 116 headers.put(value.name(), value); 117 // Check some consistency rules 118 if (value.name().equalsIgnoreCase(HttpField.UPGRADE)) { 119 computeIfAbsent(HttpField.CONNECTION, 120 Converters.STRING_LIST, StringList::new) 121 .value().appendIfNotContained(HttpField.UPGRADE); 122 } 123 return this; 124 } 125 126 /** 127 * Sets a header field for the message. The converter for the 128 * field is looked up using {@link HttpField#lookupConverter(String)}. 129 * 130 * @param <T> the type of the value 131 * @param name the field name 132 * @param value the header field's value 133 * @return the message header for easy chaining 134 */ 135 @SuppressWarnings("unchecked") 136 public <T> HttpMessageHeader setField(String name, T value) { 137 setField(new HttpField<T>( 138 name, value, (Converter<T>)HttpField.lookupConverter(name))); 139 return this; 140 } 141 142 /** 143 * Clear all headers. 144 * 145 * @return the message header for easy chaining 146 */ 147 public HttpMessageHeader clearHeaders() { 148 headers.clear(); 149 return this; 150 } 151 152 /** 153 * Removes a header field from the message. 154 * 155 * @param name the header field's name 156 * @return the message header for easy chaining 157 */ 158 public HttpMessageHeader removeField(String name) { 159 headers.remove(name); 160 return this; 161 } 162 163 /** 164 * Returns the header field with the given type if it exists. 165 * 166 * Header fields are provisionally parsed as 167 * {@link HttpField}s with value type `String`. When an attempt is 168 * made to retrieve such a string field with this method, 169 * it is automatically converted to the type indicated by the converter. 170 * The conversion is permanent, i.e. the field instance is replaced 171 * by a properly typed instance. 172 * 173 * If the conversion fails, the field is considered ill-formatted 174 * and handled as if it didn't exist. 175 * 176 * Note that field type conversion may already occur while doing internal 177 * checks. This implies that not all fields can initially be 178 * accessed as {@link HttpField}s with a `String` value. 179 * 180 * @param <T> the type of the value in the header field 181 * @param name the field name 182 * @param converter the converter for the value type 183 * @return the header field if it exists 184 */ 185 public <T> Optional<HttpField<T>> 186 findField(String name, Converter<T> converter) { 187 HttpField<?> field = headers.get(name); 188 // Not found 189 if (field == null) { 190 return Optional.ofNullable(null); 191 } 192 Object value = field.value(); 193 Class<?> valueType = value.getClass(); 194 Class<?> expectedType = null; 195 try { 196 expectedType = converter.getClass().getMethod( 197 "fromFieldValue", String.class).getReturnType(); 198 } catch (NoSuchMethodException e) { 199 // Known to exists, but make find bugs happy 200 throw new IllegalStateException(); 201 } 202 // Match already? 203 if (expectedType.isAssignableFrom(valueType)) { 204 @SuppressWarnings("unchecked") 205 HttpField<T> result = (HttpField<T>)field; 206 return Optional.of(result); 207 } 208 // String field? 209 if (!(value instanceof String)) { 210 return Optional.empty(); 211 } 212 // Try conversion... 213 try { 214 HttpField<T> converted = new HttpField<>( 215 name, converter.fromFieldValue((String)value), converter); 216 // Replace if conversion was successful 217 headers.put(name, converted); 218 return Optional.ofNullable(converted); 219 } catch (ParseException e) { 220 return Optional.empty(); 221 } 222 } 223 224 /** 225 * Convenience method for getting the value of a header field. 226 * 227 * @param <T> the type of the value in the header field 228 * @param name the field name 229 * @param converter the converter for the value type 230 * @return the value if the header field exists 231 */ 232 public <T> Optional<T> findValue(String name, Converter<T> converter) { 233 return findField(name, converter).map(HttpField<T>::value); 234 } 235 236 /** 237 * Convenience method for getting the value of a string field. 238 * 239 * @param name the field name 240 * @return the value if the header field exists 241 * @see #findField(String, Converter) 242 */ 243 public Optional<String> findStringValue(String name) { 244 return findValue(name, Converters.STRING); 245 } 246 247 /** 248 * Returns the header field with the given name, computing 249 * and adding it if it doesn't exist. 250 * 251 * @param <T> the type of the header field's value 252 * @param name the field name 253 * @param converter the converter for the value type 254 * @param supplier the function that computes a value for 255 * a new field. 256 * @return the header field 257 */ 258 public <T> HttpField<T> computeIfAbsent(String name, 259 Converter<T> converter, Supplier<T> supplier) { 260 Optional<HttpField<T>> result = findField(name, converter); 261 if (result.isPresent()) { 262 return result.get(); 263 } 264 HttpField<T> value = new HttpField<>(name, supplier.get(), converter); 265 266 setField(value); 267 return value; 268 } 269 270 /** 271 * Returns the header field with the given name, computing 272 * and adding it if it doesn't exist. The converter for the 273 * field is looked up using {@link HttpField#lookupConverter(String)}. 274 * 275 * @param <T> the type of the header field's value 276 * @param name the field name 277 * @param supplier the function that computes a value for 278 * a new field. 279 * @return the header field 280 */ 281 public <T> HttpField<T> computeIfAbsent(String name, Supplier<T> supplier) { 282 @SuppressWarnings("unchecked") 283 Converter<T> converter = (Converter<T>)HttpField.lookupConverter(name); 284 Optional<HttpField<T>> result = findField(name, converter); 285 if (result.isPresent()) { 286 return result.get(); 287 } 288 HttpField<T> value = new HttpField<>(name, supplier.get(), converter); 289 290 setField(value); 291 return value; 292 } 293 294 /** 295 * Set the flag that indicates whether this header is followed by a body. 296 * 297 * @param hasPayload new value 298 * @return the message for easy chaining 299 */ 300 public MessageHeader setHasPayload(boolean hasPayload) { 301 this.hasPayload = hasPayload; 302 return this; 303 } 304 305 /** 306 * Returns {@code true} if the header is followed by a payload body. 307 * 308 * @return {@code true} if payload body data follows 309 */ 310 @Override 311 public boolean hasPayload() { 312 return hasPayload; 313 } 314 315 /** 316 * Returns true if this is a final message. A message is 317 * final if the value of the `Connection` header field 318 * includes the value "`close`". 319 * 320 * @return the result 321 */ 322 @Override 323 public boolean isFinal() { 324 return findField(HttpField.CONNECTION, Converters.STRING_LIST) 325 .map(h -> h.value()).map(f -> f.contains("close")).orElse(false); 326 } 327}