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