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}