1 //
2 // ========================================================================
3 // Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
8 //
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
11 //
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
14 //
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
17 //
18
19 package org.eclipse.jetty.spdy.api;
20
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.Iterator;
24 import java.util.LinkedHashMap;
25 import java.util.LinkedHashSet;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Set;
29
30 /**
31 * <p>A container for name/value pairs, known as headers.</p>
32 * <p>A {@link Header} is composed of a case-insensitive name string and
33 * of a case-sensitive set of value strings.</p>
34 * <p>The implementation of this class is not thread safe.</p>
35 */
36 public class Headers implements Iterable<Headers.Header>
37 {
38 private final Map<String, Header> headers;
39
40 /**
41 * <p>Creates an empty modifiable {@link Headers} instance.</p>
42 * @see #Headers(Headers, boolean)
43 */
44 public Headers()
45 {
46 headers = new LinkedHashMap<>();
47 }
48
49 /**
50 * <p>Creates a {@link Headers} instance by copying the headers from the given
51 * {@link Headers} and making it (im)mutable depending on the given {@code immutable} parameter</p>
52 *
53 * @param original the {@link Headers} to copy headers from
54 * @param immutable whether this instance is immutable
55 */
56 public Headers(Headers original, boolean immutable)
57 {
58 Map<String, Header> copy = new LinkedHashMap<>();
59 copy.putAll(original.headers);
60 headers = immutable ? Collections.unmodifiableMap(copy) : copy;
61 }
62
63 @Override
64 public boolean equals(Object obj)
65 {
66 if (this == obj)
67 return true;
68 if (obj == null || getClass() != obj.getClass())
69 return false;
70 Headers that = (Headers)obj;
71 return headers.equals(that.headers);
72 }
73
74 @Override
75 public int hashCode()
76 {
77 return headers.hashCode();
78 }
79
80 /**
81 * @return a set of header names
82 */
83 public Set<String> names()
84 {
85 Set<String> result = new LinkedHashSet<>();
86 for (Header header : headers.values())
87 result.add(header.name);
88 return result;
89 }
90
91 /**
92 * @param name the header name
93 * @return the {@link Header} with the given name, or null if no such header exists
94 */
95 public Header get(String name)
96 {
97 return headers.get(name.trim().toLowerCase(Locale.ENGLISH));
98 }
99
100 /**
101 * <p>Inserts or replaces the given name/value pair as a single-valued {@link Header}.</p>
102 *
103 * @param name the header name
104 * @param value the header value
105 */
106 public void put(String name, String value)
107 {
108 name = name.trim();
109 Header header = new Header(name, value.trim());
110 headers.put(name.toLowerCase(Locale.ENGLISH), header);
111 }
112
113 /**
114 * <p>Inserts or replaces the given {@link Header}, mapped to the {@link Header#name() header's name}</p>
115 *
116 * @param header the header to add
117 */
118 public void put(Header header)
119 {
120 if (header != null)
121 headers.put(header.name().toLowerCase(Locale.ENGLISH), header);
122 }
123
124 /**
125 * <p>Adds the given value to a header with the given name, creating a {@link Header} is none exists
126 * for the given name.</p>
127 *
128 * @param name the header name
129 * @param value the header value to add
130 */
131 public void add(String name, String value)
132 {
133 name = name.trim();
134 Header header = headers.get(name.toLowerCase(Locale.ENGLISH));
135 if (header == null)
136 {
137 header = new Header(name, value.trim());
138 headers.put(name.toLowerCase(Locale.ENGLISH), header);
139 }
140 else
141 {
142 header = new Header(header.name(), header.value() + "," + value.trim());
143 headers.put(name.toLowerCase(Locale.ENGLISH), header);
144 }
145 }
146
147 /**
148 * <p>Removes the {@link Header} with the given name</p>
149 *
150 * @param name the name of the header to remove
151 * @return the removed header, or null if no such header existed
152 */
153 public Header remove(String name)
154 {
155 name = name.trim();
156 return headers.remove(name.toLowerCase(Locale.ENGLISH));
157 }
158
159 /**
160 * <p>Empties this {@link Headers} instance from all headers</p>
161 * @see #isEmpty()
162 */
163 public void clear()
164 {
165 headers.clear();
166 }
167
168 /**
169 * @return whether this {@link Headers} instance is empty
170 */
171 public boolean isEmpty()
172 {
173 return headers.isEmpty();
174 }
175
176 /**
177 * @return the number of headers
178 */
179 public int size()
180 {
181 return headers.size();
182 }
183
184 /**
185 * @return an iterator over the {@link Header} present in this instance
186 */
187 @Override
188 public Iterator<Header> iterator()
189 {
190 return headers.values().iterator();
191 }
192
193 @Override
194 public String toString()
195 {
196 return headers.toString();
197 }
198
199 /**
200 * <p>A named list of string values.</p>
201 * <p>The name is case-sensitive and there must be at least one value.</p>
202 */
203 public static class Header
204 {
205 private final String name;
206 private final String[] values;
207
208 private Header(String name, String value, String... values)
209 {
210 this.name = name;
211 this.values = new String[values.length + 1];
212 this.values[0] = value;
213 if (values.length > 0)
214 System.arraycopy(values, 0, this.values, 1, values.length);
215 }
216
217 @Override
218 public boolean equals(Object obj)
219 {
220 if (this == obj)
221 return true;
222 if (obj == null || getClass() != obj.getClass())
223 return false;
224 Header that = (Header)obj;
225 // Header names must be lowercase, thus we lowercase them before transmission, but keep them as is
226 // internally. That's why we've to compare them case insensitive.
227 return name.equalsIgnoreCase(that.name) && Arrays.equals(values, that.values);
228 }
229
230 @Override
231 public int hashCode()
232 {
233 int result = name.toLowerCase(Locale.ENGLISH).hashCode();
234 result = 31 * result + Arrays.hashCode(values);
235 return result;
236 }
237
238 /**
239 * @return the header's name
240 */
241 public String name()
242 {
243 return name;
244 }
245
246 /**
247 * @return the first header's value
248 */
249 public String value()
250 {
251 return values[0];
252 }
253
254 /**
255 * <p>Attempts to convert the result of {@link #value()} to an integer,
256 * returning it if the conversion is successful; returns null if the
257 * result of {@link #value()} is null.</p>
258 *
259 * @return the result of {@link #value()} converted to an integer, or null
260 * @throws NumberFormatException if the conversion fails
261 */
262 public Integer valueAsInt()
263 {
264 final String value = value();
265 return value == null ? null : Integer.valueOf(value);
266 }
267
268 /**
269 * @return the header's values
270 */
271 public String[] values()
272 {
273 return values;
274 }
275
276 /**
277 * @return the values as a comma separated list
278 */
279 public String valuesAsString()
280 {
281 StringBuilder result = new StringBuilder();
282 for (int i = 0; i < values.length; ++i)
283 {
284 if (i > 0)
285 result.append(", ");
286 result.append(values[i]);
287 }
288 return result.toString();
289 }
290
291 /**
292 * @return whether the header has multiple values
293 */
294 public boolean hasMultipleValues()
295 {
296 return values.length > 1;
297 }
298
299 @Override
300 public String toString()
301 {
302 return Arrays.toString(values);
303 }
304 }
305 }