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.util;
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 fields.</p>
32 * <p>A {@link Field} 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 Fields implements Iterable<Fields.Field>
37 {
38 private final Map<String, Field> fields;
39
40 /**
41 * <p>Creates an empty modifiable {@link Fields} instance.</p>
42 * @see #Fields(Fields, boolean)
43 */
44 public Fields()
45 {
46 fields = new LinkedHashMap<>();
47 }
48
49 /**
50 * <p>Creates a {@link Fields} instance by copying the fields from the given
51 * {@link Fields} and making it (im)mutable depending on the given {@code immutable} parameter</p>
52 *
53 * @param original the {@link Fields} to copy fields from
54 * @param immutable whether this instance is immutable
55 */
56 public Fields(Fields original, boolean immutable)
57 {
58 Map<String, Field> copy = new LinkedHashMap<>();
59 copy.putAll(original.fields);
60 fields = 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 Fields that = (Fields)obj;
71 return fields.equals(that.fields);
72 }
73
74 @Override
75 public int hashCode()
76 {
77 return fields.hashCode();
78 }
79
80 /**
81 * @return a set of field names
82 */
83 public Set<String> names()
84 {
85 Set<String> result = new LinkedHashSet<>();
86 for (Field field : fields.values())
87 result.add(field.name());
88 return result;
89 }
90
91 /**
92 * @param name the field name
93 * @return the {@link Field} with the given name, or null if no such field exists
94 */
95 public Field get(String name)
96 {
97 return fields.get(name.trim().toLowerCase(Locale.ENGLISH));
98 }
99
100 /**
101 * <p>Inserts or replaces the given name/value pair as a single-valued {@link Field}.</p>
102 *
103 * @param name the field name
104 * @param value the field value
105 */
106 public void put(String name, String value)
107 {
108 name = name.trim();
109 // Preserve the case for the field name
110 Field field = new Field(name, value);
111 fields.put(name.toLowerCase(Locale.ENGLISH), field);
112 }
113
114 /**
115 * <p>Inserts or replaces the given {@link Field}, mapped to the {@link Field#name() field's name}</p>
116 *
117 * @param field the field to put
118 */
119 public void put(Field field)
120 {
121 if (field != null)
122 fields.put(field.name().toLowerCase(Locale.ENGLISH), field);
123 }
124
125 /**
126 * <p>Adds the given value to a field with the given name,
127 * creating a {@link Field} is none exists for the given name.</p>
128 *
129 * @param name the field name
130 * @param value the field value to add
131 */
132 public void add(String name, String value)
133 {
134 name = name.trim();
135 Field field = fields.get(name.toLowerCase(Locale.ENGLISH));
136 if (field == null)
137 {
138 field = new Field(name, value);
139 fields.put(name.toLowerCase(Locale.ENGLISH), field);
140 }
141 else
142 {
143 field = new Field(field.name(), field.values(), value);
144 fields.put(name.toLowerCase(Locale.ENGLISH), field);
145 }
146 }
147
148 /**
149 * <p>Removes the {@link Field} with the given name</p>
150 *
151 * @param name the name of the field to remove
152 * @return the removed field, or null if no such field existed
153 */
154 public Field remove(String name)
155 {
156 name = name.trim();
157 return fields.remove(name.toLowerCase(Locale.ENGLISH));
158 }
159
160 /**
161 * <p>Empties this {@link Fields} instance from all fields</p>
162 * @see #isEmpty()
163 */
164 public void clear()
165 {
166 fields.clear();
167 }
168
169 /**
170 * @return whether this {@link Fields} instance is empty
171 */
172 public boolean isEmpty()
173 {
174 return fields.isEmpty();
175 }
176
177 /**
178 * @return the number of fields
179 */
180 public int size()
181 {
182 return fields.size();
183 }
184
185 /**
186 * @return an iterator over the {@link Field}s present in this instance
187 */
188 @Override
189 public Iterator<Field> iterator()
190 {
191 return fields.values().iterator();
192 }
193
194 @Override
195 public String toString()
196 {
197 return fields.toString();
198 }
199
200 /**
201 * <p>A named list of string values.</p>
202 * <p>The name is case-sensitive and there must be at least one value.</p>
203 */
204 public static class Field
205 {
206 private final String name;
207 private final String[] values;
208
209 private Field(String name, String value)
210 {
211 this(name, new String[]{value});
212 }
213
214 private Field(String name, String[] values, String... moreValues)
215 {
216 this.name = name;
217 this.values = new String[values.length + moreValues.length];
218 System.arraycopy(values, 0, this.values, 0, values.length);
219 System.arraycopy(moreValues, 0, this.values, values.length, moreValues.length);
220 }
221
222 @Override
223 public boolean equals(Object obj)
224 {
225 if (this == obj)
226 return true;
227 if (obj == null || getClass() != obj.getClass())
228 return false;
229 Field that = (Field)obj;
230 // Field names must be lowercase, thus we lowercase them before transmission, but keep them as is
231 // internally. That's why we've to compare them case insensitive.
232 return name.equalsIgnoreCase(that.name) && Arrays.equals(values, that.values);
233 }
234
235 @Override
236 public int hashCode()
237 {
238 int result = name.toLowerCase(Locale.ENGLISH).hashCode();
239 result = 31 * result + Arrays.hashCode(values);
240 return result;
241 }
242
243 /**
244 * @return the field's name
245 */
246 public String name()
247 {
248 return name;
249 }
250
251 /**
252 * @return the first field's value
253 */
254 public String value()
255 {
256 return values[0];
257 }
258
259 /**
260 * <p>Attempts to convert the result of {@link #value()} to an integer,
261 * returning it if the conversion is successful; returns null if the
262 * result of {@link #value()} is null.</p>
263 *
264 * @return the result of {@link #value()} converted to an integer, or null
265 * @throws NumberFormatException if the conversion fails
266 */
267 public Integer valueAsInt()
268 {
269 final String value = value();
270 return value == null ? null : Integer.valueOf(value);
271 }
272
273 /**
274 * @return the field's values
275 */
276 public String[] values()
277 {
278 return values;
279 }
280
281 /**
282 * @return whether the field has multiple values
283 */
284 public boolean hasMultipleValues()
285 {
286 return values.length > 1;
287 }
288
289 @Override
290 public String toString()
291 {
292 return Arrays.toString(values);
293 }
294 }
295 }