001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.request;
018
019import java.sql.Time;
020import java.time.Instant;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Set;
030import org.apache.wicket.util.lang.Args;
031import org.apache.wicket.util.time.Instants;
032
033/**
034 * A multivalue map of headers names and header values suitable for processing http request and
035 * response headers.
036 * 
037 * @author Peter Ertl
038 * 
039 * @since 1.5
040 */
041public class HttpHeaderCollection
042{
043        private final Map<HeaderKey, List<Object>> headers;
044
045        /** returned in case no header values were found */
046        private static final String[] NO_VALUES = new String[0];
047
048        /**
049         * Constructor.
050         */
051        public HttpHeaderCollection()
052        {
053                headers = new HashMap<>();
054        }
055
056        /**
057         * internally add new object to header values
058         * 
059         * @param name
060         *            header name
061         * @param object
062         *            header value (can be a string or a {@link Time} object
063         */
064        private void internalAdd(String name, Object object)
065        {
066                final HeaderKey key = new HeaderKey(name);
067
068                List<Object> values = headers.get(key);
069
070                if (values == null)
071                {
072                        values = new ArrayList<>();
073                        headers.put(key, values);
074                }
075                values.add(object);
076        }
077
078        /**
079         * set header value (and remove previous values)
080         * 
081         * @param name
082         *            header name
083         * @param value
084         *            header value
085         */
086        public void setHeader(String name, String value)
087        {
088                // remove previous values
089                removeHeader(name);
090
091                // add new values
092                addHeader(name, value);
093        }
094
095        /**
096         * add header value
097         * 
098         * @param name
099         *            header name
100         * @param value
101         *            header value
102         */
103        public void addHeader(String name, String value)
104        {
105                // be lenient and strip leading / trailing blanks
106                value = Args.notNull(value, "value").trim();
107
108                internalAdd(name, value);
109        }
110
111        /**
112         * add date header value
113         * 
114         * @param name
115         *            header name
116         * @param time
117         *            timestamp
118         */
119        public void addDateHeader(String name, Instant time)
120        {
121                internalAdd(name, time);
122        }
123
124        /**
125         * add date header value
126         * 
127         * @param name
128         *            header name
129         * @param time
130         *            timestamp
131         */
132        public void setDateHeader(String name, Instant time)
133        {
134                // remove previous values
135                removeHeader(name);
136
137                // add time object to values
138                addDateHeader(name, time);
139        }
140
141        /**
142         * remove header values for header name
143         * 
144         * @param name
145         *            header name
146         */
147        public void removeHeader(String name)
148        {
149                final HeaderKey key = new HeaderKey(name);
150                final Iterator<Map.Entry<HeaderKey, List<Object>>> it = headers.entrySet().iterator();
151
152                while (it.hasNext())
153                {
154                        final Map.Entry<HeaderKey, List<Object>> header = it.next();
155
156                        if (header.getKey().equals(key))
157                        {
158                                it.remove();
159                        }
160                }
161        }
162
163        private String valueToString(Object value)
164        {
165                if (value instanceof Instant)
166                {
167                        return Instants.toRFC7231Format((Instant)value);
168                }
169                else
170                {
171                        return value.toString();
172                }
173        }
174
175        /**
176         * check if header is defined
177         * 
178         * @param name
179         *            header name
180         * @return <code>true</code> if header has one or more values
181         */
182        public boolean containsHeader(String name)
183        {
184                final HeaderKey searchKey = new HeaderKey(name);
185
186                // get the header value (case might differ)
187                for (HeaderKey key : headers.keySet())
188                {
189                        if (key.equals(searchKey))
190                        {
191                                return true;
192                        }
193                }
194                return false;
195        }
196
197        /**
198         * returns names of headers
199         * 
200         * @return set of header names
201         */
202        public Set<String> getHeaderNames()
203        {
204                if (headers.isEmpty())
205                {
206                        return Collections.emptySet();
207                }
208
209                final Set<String> names = new HashSet<>(headers.size());
210
211                for (HeaderKey key : headers.keySet())
212                {
213                        names.add(key.getName());
214                }
215                return names;
216        }
217
218        /**
219         * get header values (dates will be converted into strings)
220         * 
221         * @param name
222         *            header name
223         * 
224         * @return array of header values or empty array if not found
225         */
226        public String[] getHeaderValues(String name)
227        {
228                final List<Object> objects = headers.get(new HeaderKey(name));
229
230                if (objects == null)
231                {
232                        return NO_VALUES;
233                }
234
235                final String[] values = new String[objects.size()];
236
237                for (int i = 0; i < values.length; i++)
238                {
239                        values[i] = valueToString(objects.get(i));
240                }
241                return values;
242        }
243
244        /**
245         * Gets the header identified with the name as a String.
246         * @param name
247         * @return {@code null} when the header was not found
248         */
249        public String getHeader(String name)
250        {
251                final List<Object> objects = headers.get(new HeaderKey(name));
252
253                if (objects == null || objects.isEmpty())
254                {
255                        return null;
256                }
257                return valueToString(objects.get(0));
258        }
259
260        /**
261         * Gets the header identified with the name as a Time
262         * @param name
263         * @return {@code null} when the header was not found
264         */
265        public Instant getDateHeader(String name)
266        {
267                final List<Object> objects = headers.get(new HeaderKey(name));
268
269                if (objects.isEmpty())
270                {
271                        return null;
272                }
273                Object object = objects.get(0);
274
275                if ((object instanceof Instant) == false)
276                {
277                        throw new IllegalStateException("header value is not of type date");
278                }
279                return (Instant)object;
280        }
281
282        /**
283         * Check if collection is empty
284         * 
285         * @return <code>true</code> if collection is empty, <code>false</code> otherwise
286         */
287        public boolean isEmpty()
288        {
289                return headers.isEmpty();
290        }
291
292        /**
293         * get number of headers
294         * 
295         * @return count
296         */
297        public int getCount()
298        {
299                return headers.size();
300        }
301
302        /**
303         * clear all headers
304         */
305        public void clear()
306        {
307                headers.clear();
308        }
309
310        /**
311         * key for header collection
312         */
313        private static class HeaderKey
314        {
315                private final String key;
316                private final String name;
317
318                private HeaderKey(String name)
319                {
320                        this.name = Args.notEmpty(name, "name").trim();
321                        key = this.name.toLowerCase(Locale.US);
322                }
323
324                public String getName()
325                {
326                        return name;
327                }
328
329                @Override
330                public boolean equals(Object o)
331                {
332                        if (this == o)
333                                return true;
334
335                        if (!(o instanceof HeaderKey))
336                                return false;
337
338                        HeaderKey that = (HeaderKey)o;
339
340                        if (!key.equals(that.key))
341                                return false;
342
343                        return true;
344                }
345
346                @Override
347                public int hashCode()
348                {
349                        return key.hashCode();
350                }
351        }
352}