001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2009 Sun Microsystems Inc.
015 * Portions Copyright 2010–2011 ApexIdentity Inc.
016 * Portions Copyright 2011-2016 ForgeRock AS.
017 */
018
019package org.forgerock.http.protocol;
020
021import static org.forgerock.http.header.HeaderFactory.*;
022
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.TreeMap;
030
031import org.forgerock.http.header.GenericHeader;
032import org.forgerock.http.header.HeaderFactory;
033import org.forgerock.http.header.MalformedHeaderException;
034
035/**
036 * Message headers, a case-insensitive multiple-value map.
037 */
038public class Headers implements Map<String, Object> {
039
040    private final Map<String, Header> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
041
042    /**
043     * Constructs a {@code Headers} object that is case-insensitive for header names.
044     */
045    public Headers() { }
046
047    /**
048     * Defensive copy constructor.
049     */
050    Headers(final Headers headers) {
051        // Force header re-creation
052        for (Map.Entry<String, Header> entry : headers.asMapOfHeaders().entrySet()) {
053            add(entry.getKey(), entry.getValue().getValues());
054        }
055    }
056
057    /**
058     * Rich-type friendly get method.
059     * @param key The name of the header.
060     * @return The header object.
061     */
062    @Override
063    public Header get(Object key) {
064        return headers.get(key);
065    }
066
067    /**
068     * Gets the first value of the header, or null if the header does not exist.
069     * @param key The name of the header.
070     * @return The first header value.
071     */
072    public String getFirst(String key) {
073        final Header header = headers.get(key);
074        return header == null ? null : header.getFirstValue();
075    }
076
077    /**
078     * Gets the first value of the header, or null if the header does not exist.
079     * @param key The name of the header.
080     * @return The first header value.
081     */
082    public String getFirst(Class<? extends Header> key) {
083        final Header header = headers.get(getHeaderName(key));
084        return header == null ? null : header.getFirstValue();
085    }
086
087    /**
088     * Returns the specified {@link Header} or {code null} if the header is not included in the message.
089     *
090     * @param headerType The type of header.
091     * @param <H> The type of header.
092     * @return The header instance, or null if none exists.
093     * @throws MalformedHeaderException When the header was not well formed, and so could not be parsed as
094     * its richly-typed class.
095     */
096    public <H extends Header> H get(Class<H> headerType) throws MalformedHeaderException {
097        final Header header = this.get(getHeaderName(headerType));
098        if (header instanceof GenericHeader) {
099            throw new MalformedHeaderException("Header value(s) are not well formed");
100        }
101        return headerType.cast(header);
102    }
103
104    private <H extends Header> String getHeaderName(Class<H> headerType) {
105        final String headerName = HEADER_NAMES.get(headerType);
106        if (headerName == null) {
107            throw new IllegalArgumentException("Unknown header type: " + headerType);
108        }
109        return headerName;
110    }
111
112    /**
113     * A script compatible putAll method that will accept {@code Header}, {@code String}, {@code Collection<String>}
114     * and {@code String[]} values.
115     * @param m A map of header names to values.
116     */
117    @Override
118    public void putAll(Map<? extends String, ? extends Object> m) {
119        for (Map.Entry<? extends String, ? extends Object> entry : m.entrySet()) {
120            put(entry.getKey(), entry.getValue());
121        }
122    }
123
124    /**
125     * A script compatible put method that will accept a {@code Header}, {@code String}, {@code Collection<String>}
126     * and {@code String[]} value.
127     * @param key The name of the header.
128     * @param value A {@code Header}, {@code String}, {@code Collection<String>} or {@code String[]}.
129     * @return The previous {@code Header} value for this header, or null.
130     */
131    @Override
132    public Header put(String key, Object value) {
133        if (value == null) {
134            return remove(key);
135        }
136        final HeaderFactory<?> factory = FACTORIES.get(key);
137        if (value instanceof Header) {
138            return putHeader(key, (Header) value, factory);
139        } else if (factory != null) {
140            return putUsingFactory(key, value, factory);
141        } else {
142            return putGenericHeader(key, value);
143        }
144    }
145
146    private Header putGenericHeader(String key, Object value) {
147        if (value instanceof String) {
148            return putGenericString(key, (String) value);
149        } else if (value instanceof List) {
150            return putGenericList(key, (List<?>) value);
151        } else if (value instanceof Collection) {
152            return putGenericList(key, new ArrayList<>((Collection<?>) value));
153        } else if (value.getClass().isArray()) {
154            return putGenericList(key, Arrays.asList((Object[]) value));
155        }
156        throw new IllegalArgumentException("Cannot put object for key '" + key + "': " + value);
157    }
158
159    private Header putHeader(String key, Header header, HeaderFactory<?> factory) {
160        if (!hasAnyValue(header)) {
161            return remove(key);
162        }
163        if (HEADER_NAMES.containsValue(key) && !HEADER_NAMES.get(header.getClass()).equals(key)) {
164            if (header instanceof GenericHeader) {
165                return putUsingFactory(key, header.getValues(), factory);
166            }
167            throw new IllegalArgumentException("Header object of incorrect type for header " + key);
168        }
169        return headers.put(key, header);
170    }
171
172    private boolean hasAnyValue(Header header) {
173        boolean hasValue = false;
174        for (String s : header.getValues()) {
175            if (s != null) {
176                hasValue = true;
177                break;
178            }
179        }
180        return hasValue;
181    }
182
183    @SuppressWarnings("unchecked")
184    private Header putGenericList(String key, List<?> value) {
185        if (value.isEmpty()) {
186            return remove(key);
187        }
188        for (Object o : value) {
189            if (!(o instanceof String)) {
190                throw new IllegalArgumentException("Collections must be of strings");
191            }
192        }
193        return headers.put(key, new GenericHeader(key, (List<String>) value));
194    }
195
196    private Header putGenericString(String key, String value) {
197        return headers.put(key, new GenericHeader(key, value));
198    }
199
200    private Header putUsingFactory(String key, Object value, HeaderFactory<?> factory) {
201        final Header parsed;
202        try {
203            parsed = factory.parse(value);
204        } catch (MalformedHeaderException e) {
205            if (value instanceof Header) {
206                value = ((Header) value).getValues();
207            }
208            return putGenericHeader(key, value);
209        }
210        if (parsed == null) {
211            return remove(key);
212        }
213        return headers.put(key, parsed);
214    }
215
216    /**
217     * Rich-type friendly remove method. Removes the {@code Header} object for the given header name.
218     * @param key The header name.
219     * @return The header value before removal, or null.
220     */
221    @Override
222    public Header remove(Object key) {
223        return headers.remove(key);
224    }
225
226    /**
227     * A put method to add a particular {@code Header} instance. Will overwrite any existing value for this
228     * header name.
229     * @param header The header instance.
230     * @return The previous {@code Header} value for the header with the same name, or null.
231     */
232    public Header put(Header header) {
233        return put(header.getName(), header);
234    }
235
236    /**
237     * An add method to add a particular {@code Header} instance. Existing values for the header will be added to.
238     *
239     * @param header The header instance.
240     */
241    public void add(Header header) {
242        add(header.getName(), header);
243    }
244
245    /**
246     * A script compatible add method that will accept a {@code Header}, {@code String}, {@code Collection<String>}
247     * and {@code String[]} value. Existing values for the header will be added to.
248     * @param key The name of the header.
249     * @param value A {@code Header}, {@code String}, {@code Collection<String>} or {@code String[]}.
250     */
251    @SuppressWarnings("unchecked")
252    public void add(String key, Object value) {
253        if (value == null) {
254            return;
255        }
256        List<String> values = containsKey(key) ? new ArrayList<>(get(key).getValues()) : new ArrayList<String>();
257        if (value instanceof Header) {
258            for (String s : ((Header) value).getValues()) {
259                addNonNullStringValue(values, s);
260            }
261        } else if (value instanceof String) {
262            values.add((String) value);
263        } else if (value instanceof Collection) {
264            final Collection<String> collection = (Collection<String>) value;
265            for (String s : collection) {
266                addNonNullStringValue(values, s);
267            }
268        } else if (value.getClass().isArray()) {
269            String[] array = (String[]) value;
270            for (String s : array) {
271                addNonNullStringValue(values, s);
272            }
273        } else {
274            throw new IllegalArgumentException("Cannot add values for key '" + key + "': " + value);
275        }
276        if (values.isEmpty()) {
277            return;
278        }
279        final HeaderFactory<?> factory = FACTORIES.get(key);
280        if (factory != null) {
281            Header parsed;
282            try {
283                parsed = factory.parse(values);
284            } catch (MalformedHeaderException e) {
285                parsed = new GenericHeader(key, values);
286            }
287            if (parsed == null) {
288                return;
289            }
290            headers.put(key, parsed);
291        } else {
292            headers.put(key, new GenericHeader(key, values));
293        }
294    }
295
296    private void addNonNullStringValue(List<String> values, String s) {
297        if (s != null) {
298            values.add(s);
299        }
300    }
301
302    /**
303     * A script compatible addAll method that will accept a {@code Header}, {@code String}, {@code Collection<String>}
304     * and {@code String[]} value. Existing values for the headers will be added to.
305     * @param map A map of header names to values.
306     */
307    public void addAll(Map<? extends String, ? extends Object> map) {
308        for (Map.Entry<? extends String, ? extends Object> entry : map.entrySet()) {
309            add(entry.getKey(), entry.getValue());
310        }
311    }
312
313    @Override
314    public int size() {
315        return headers.size();
316    }
317
318    @Override
319    public boolean isEmpty() {
320        return headers.isEmpty();
321    }
322
323    @Override
324    public boolean containsKey(Object key) {
325        return headers.containsKey(key);
326    }
327
328    @Override
329    public boolean containsValue(Object value) {
330        return headers.containsValue(value);
331    }
332
333    @Override
334    public void clear() {
335        headers.clear();
336    }
337
338    @Override
339    public Set<String> keySet() {
340        return headers.keySet();
341    }
342
343    @Override
344    @SuppressWarnings("unchecked")
345    public Collection<Object> values() {
346        return (Collection) headers.values();
347    }
348
349    @Override
350    @SuppressWarnings("unchecked")
351    public Set<Entry<String, Object>> entrySet() {
352        return (Set) headers.entrySet();
353    }
354
355    /**
356     * The {@code Headers} class extends {@code Map<String, Object>} to support flexible parameters in scripting. This
357     * method allows access to the underlying {@code Map<String, Header>}.
358     * @return The map of header names to {@link Header} objects.
359     */
360    public Map<String, Header> asMapOfHeaders() {
361        return headers;
362    }
363
364    /**
365     * Returns a copy of these headers as a multi-valued map of strings. Changes to the returned map will not be
366     * reflected in these headers, nor will changes in these headers be reflected in the returned map.
367     *
368     * @return a copy of these headers as a multi-valued map of strings.
369     */
370    public Map<String, List<String>> copyAsMultiMapOfStrings() {
371        Map<String, List<String>> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
372        for (Header header : headers.values()) {
373            result.put(header.getName(), new ArrayList<>(header.getValues()));
374        }
375        return result;
376    }
377
378}