View Javadoc
1   /*
2    * Copyright 2017 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import io.netty.util.AsciiString;
19  import io.netty.util.internal.UnstableApi;
20  
21  import java.util.AbstractMap.SimpleImmutableEntry;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.Iterator;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.NoSuchElementException;
29  import java.util.Set;
30  
31  import static io.netty.handler.codec.CharSequenceValueConverter.INSTANCE;
32  import static io.netty.util.AsciiString.contentEquals;
33  import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
34  
35  /**
36   * A variant of {@link HttpHeaders} which only supports read-only methods.
37   * <p>
38   * Any array passed to this class may be used directly in the underlying data structures of this class. If these
39   * arrays may be modified it is the caller's responsibility to supply this class with a copy of the array.
40   * <p>
41   * This may be a good alternative to {@link DefaultHttpHeaders} if your have a fixed set of headers which will not
42   * change.
43   */
44  @UnstableApi
45  public final class ReadOnlyHttpHeaders extends HttpHeaders {
46      private final CharSequence[] nameValuePairs;
47  
48      /**
49       * Create a new instance.
50       * @param validateHeaders {@code true} to validate the contents of each header name and value.
51       * @param nameValuePairs An array of the structure {@code [<name,value>,<name,value>,...]}.
52       *                      A copy will <strong>NOT</strong> be made of this array. If the contents of this array
53       *                      may be modified externally you are responsible for passing in a copy.
54       */
55      public ReadOnlyHttpHeaders(boolean validateHeaders, CharSequence... nameValuePairs) {
56          if ((nameValuePairs.length & 1) != 0) {
57              throw newInvalidArraySizeException();
58          }
59          if (validateHeaders) {
60              validateHeaders(nameValuePairs);
61          }
62          this.nameValuePairs = nameValuePairs;
63      }
64  
65      private static IllegalArgumentException newInvalidArraySizeException() {
66          return new IllegalArgumentException("nameValuePairs must be arrays of [name, value] pairs");
67      }
68  
69      private static void validateHeaders(CharSequence... keyValuePairs) {
70          DefaultHttpHeadersFactory factory = DefaultHttpHeadersFactory.headersFactory();
71          for (int i = 0; i < keyValuePairs.length; i += 2) {
72              factory.getNameValidator().validateName(keyValuePairs[i]);
73              factory.getValueValidator().validate(keyValuePairs[i + 1]);
74          }
75      }
76  
77      private CharSequence get0(CharSequence name) {
78          final int nameHash = AsciiString.hashCode(name);
79          for (int i = 0; i < nameValuePairs.length; i += 2) {
80              CharSequence roName = nameValuePairs[i];
81              if (AsciiString.hashCode(roName) == nameHash && contentEqualsIgnoreCase(roName, name)) {
82                  // Suppress a warning out of bounds access since the constructor allows only pairs
83                  return nameValuePairs[i + 1];
84              }
85          }
86          return null;
87      }
88  
89      @Override
90      public String get(String name) {
91          CharSequence value = get0(name);
92          return value == null ? null : value.toString();
93      }
94  
95      @Override
96      public Integer getInt(CharSequence name) {
97          CharSequence value = get0(name);
98          if (value == null) {
99              return null;
100         }
101         try {
102             return INSTANCE.convertToInt(value);
103         } catch (RuntimeException ignore) {
104             return null;
105         }
106     }
107 
108     @Override
109     public int getInt(CharSequence name, int defaultValue) {
110         CharSequence value = get0(name);
111         if (value == null) {
112             return defaultValue;
113         }
114         try {
115             return INSTANCE.convertToInt(value);
116         } catch (RuntimeException ignore) {
117             return defaultValue;
118         }
119     }
120 
121     @Override
122     public Short getShort(CharSequence name) {
123         CharSequence value = get0(name);
124         if (value == null) {
125             return null;
126         }
127         try {
128             return INSTANCE.convertToShort(value);
129         } catch (RuntimeException ignore) {
130             return null;
131         }
132     }
133 
134     @Override
135     public short getShort(CharSequence name, short defaultValue) {
136         CharSequence value = get0(name);
137         if (value == null) {
138             return defaultValue;
139         }
140         try {
141             return INSTANCE.convertToShort(value);
142         } catch (RuntimeException ignore) {
143             return defaultValue;
144         }
145     }
146 
147     @Override
148     public Long getTimeMillis(CharSequence name) {
149         CharSequence value = get0(name);
150         return value == null ? null : INSTANCE.convertToTimeMillis(value);
151     }
152 
153     @Override
154     public long getTimeMillis(CharSequence name, long defaultValue) {
155         CharSequence value = get0(name);
156         return value == null ? defaultValue : INSTANCE.convertToTimeMillis(value);
157     }
158 
159     @Override
160     public List<String> getAll(String name) {
161         if (isEmpty()) {
162             return Collections.emptyList();
163         }
164         final int nameHash = AsciiString.hashCode(name);
165         List<String> values = new ArrayList<String>(4);
166         for (int i = 0; i < nameValuePairs.length; i += 2) {
167             CharSequence roName = nameValuePairs[i];
168             if (AsciiString.hashCode(roName) == nameHash && contentEqualsIgnoreCase(roName, name)) {
169                 values.add(nameValuePairs[i + 1].toString());
170             }
171         }
172         return values;
173     }
174 
175     @Override
176     public List<Map.Entry<String, String>> entries() {
177         if (isEmpty()) {
178             return Collections.emptyList();
179         }
180         List<Map.Entry<String, String>> entries = new ArrayList<Map.Entry<String, String>>(size());
181         for (int i = 0; i < nameValuePairs.length; i += 2) {
182             entries.add(new SimpleImmutableEntry<String, String>(nameValuePairs[i].toString(),
183                     nameValuePairs[i + 1].toString())); // [java/index-out-of-bounds]
184         }
185         return entries;
186     }
187 
188     @Override
189     public boolean contains(String name) {
190         return get0(name) != null;
191     }
192 
193     @Override
194     public boolean contains(String name, String value, boolean ignoreCase) {
195         return containsValue(name, value, ignoreCase);
196     }
197 
198     @Override
199     public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
200         if (ignoreCase) {
201             for (int i = 0; i < nameValuePairs.length; i += 2) {
202                 if (contentEqualsIgnoreCase(nameValuePairs[i], name) &&
203                         contentEqualsIgnoreCase(nameValuePairs[i + 1], value)) {
204                     return true;
205                 }
206             }
207         } else {
208             for (int i = 0; i < nameValuePairs.length; i += 2) {
209                 if (contentEqualsIgnoreCase(nameValuePairs[i], name) &&
210                         contentEquals(nameValuePairs[i + 1], value)) {
211                     return true;
212                 }
213             }
214         }
215         return false;
216     }
217 
218     @Override
219     public Iterator<String> valueStringIterator(CharSequence name) {
220         return new ReadOnlyStringValueIterator(name);
221     }
222 
223     @Override
224     public Iterator<CharSequence> valueCharSequenceIterator(CharSequence name) {
225         return new ReadOnlyValueIterator(name);
226     }
227 
228     @Override
229     public Iterator<Map.Entry<String, String>> iterator() {
230         return new ReadOnlyStringIterator();
231     }
232 
233     @Override
234     public Iterator<Map.Entry<CharSequence, CharSequence>> iteratorCharSequence() {
235         return new ReadOnlyIterator();
236     }
237 
238     @Override
239     public boolean isEmpty() {
240         return nameValuePairs.length == 0;
241     }
242 
243     @Override
244     public int size() {
245         return nameValuePairs.length >>> 1;
246     }
247 
248     @Override
249     public Set<String> names() {
250         if (isEmpty()) {
251             return Collections.emptySet();
252         }
253         Set<String> names = new LinkedHashSet<String>(size());
254         for (int i = 0; i < nameValuePairs.length; i += 2) {
255             names.add(nameValuePairs[i].toString());
256         }
257         return names;
258     }
259 
260     @Override
261     public HttpHeaders add(String name, Object value) {
262         throw new UnsupportedOperationException("read only");
263     }
264 
265     @Override
266     public HttpHeaders add(String name, Iterable<?> values) {
267         throw new UnsupportedOperationException("read only");
268     }
269 
270     @Override
271     public HttpHeaders addInt(CharSequence name, int value) {
272         throw new UnsupportedOperationException("read only");
273     }
274 
275     @Override
276     public HttpHeaders addShort(CharSequence name, short value) {
277         throw new UnsupportedOperationException("read only");
278     }
279 
280     @Override
281     public HttpHeaders set(String name, Object value) {
282         throw new UnsupportedOperationException("read only");
283     }
284 
285     @Override
286     public HttpHeaders set(String name, Iterable<?> values) {
287         throw new UnsupportedOperationException("read only");
288     }
289 
290     @Override
291     public HttpHeaders setInt(CharSequence name, int value) {
292         throw new UnsupportedOperationException("read only");
293     }
294 
295     @Override
296     public HttpHeaders setShort(CharSequence name, short value) {
297         throw new UnsupportedOperationException("read only");
298     }
299 
300     @Override
301     public HttpHeaders remove(String name) {
302         throw new UnsupportedOperationException("read only");
303     }
304 
305     @Override
306     public HttpHeaders clear() {
307         throw new UnsupportedOperationException("read only");
308     }
309 
310     private final class ReadOnlyIterator implements Map.Entry<CharSequence, CharSequence>,
311             Iterator<Map.Entry<CharSequence, CharSequence>> {
312         private CharSequence key;
313         private CharSequence value;
314         private int nextNameIndex;
315 
316         @Override
317         public boolean hasNext() {
318             return nextNameIndex != nameValuePairs.length;
319         }
320 
321         @Override
322         public Map.Entry<CharSequence, CharSequence> next() {
323             if (!hasNext()) {
324                 throw new NoSuchElementException();
325             }
326             key = nameValuePairs[nextNameIndex];
327             value = nameValuePairs[nextNameIndex + 1];
328             nextNameIndex += 2;
329             return this;
330         }
331 
332         @Override
333         public void remove() {
334             throw new UnsupportedOperationException("read only");
335         }
336 
337         @Override
338         public CharSequence getKey() {
339             return key;
340         }
341 
342         @Override
343         public CharSequence getValue() {
344             return value;
345         }
346 
347         @Override
348         public CharSequence setValue(CharSequence value) {
349             throw new UnsupportedOperationException("read only");
350         }
351 
352         @Override
353         public String toString() {
354             return key.toString() + '=' + value.toString();
355         }
356     }
357 
358     private final class ReadOnlyStringIterator implements Map.Entry<String, String>,
359             Iterator<Map.Entry<String, String>> {
360         private String key;
361         private String value;
362         private int nextNameIndex;
363 
364         @Override
365         public boolean hasNext() {
366             return nextNameIndex != nameValuePairs.length;
367         }
368 
369         @Override
370         public Map.Entry<String, String> next() {
371             if (!hasNext()) {
372                 throw new NoSuchElementException();
373             }
374             key = nameValuePairs[nextNameIndex].toString();
375             value = nameValuePairs[nextNameIndex + 1].toString();
376             nextNameIndex += 2;
377             return this;
378         }
379 
380         @Override
381         public void remove() {
382             throw new UnsupportedOperationException("read only");
383         }
384 
385         @Override
386         public String getKey() {
387             return key;
388         }
389 
390         @Override
391         public String getValue() {
392             return value;
393         }
394 
395         @Override
396         public String setValue(String value) {
397             throw new UnsupportedOperationException("read only");
398         }
399 
400         @Override
401         public String toString() {
402             return key + '=' + value;
403         }
404     }
405 
406     private final class ReadOnlyStringValueIterator implements Iterator<String> {
407         private final CharSequence name;
408         private final int nameHash;
409         private int nextNameIndex;
410 
411         ReadOnlyStringValueIterator(CharSequence name) {
412             this.name = name;
413             nameHash = AsciiString.hashCode(name);
414             nextNameIndex = findNextValue(0);
415         }
416 
417         @Override
418         public boolean hasNext() {
419             return nextNameIndex != -1;
420         }
421 
422         @Override
423         public String next() {
424             if (!hasNext()) {
425                 throw new NoSuchElementException();
426             }
427             String value = nameValuePairs[nextNameIndex + 1].toString();
428             nextNameIndex = findNextValue(nextNameIndex + 2);
429             return value;
430         }
431 
432         @Override
433         public void remove() {
434             throw new UnsupportedOperationException("read only");
435         }
436 
437         private int findNextValue(int fromIndex) {
438             for (int i = fromIndex; i < nameValuePairs.length; i += 2) {
439                 final CharSequence roName = nameValuePairs[i];
440                 if (nameHash == AsciiString.hashCode(roName) && contentEqualsIgnoreCase(name, roName)) {
441                     return i;
442                 }
443             }
444             return -1;
445         }
446     }
447 
448     private final class ReadOnlyValueIterator implements Iterator<CharSequence> {
449         private final CharSequence name;
450         private final int nameHash;
451         private int nextNameIndex;
452 
453         ReadOnlyValueIterator(CharSequence name) {
454             this.name = name;
455             nameHash = AsciiString.hashCode(name);
456             nextNameIndex = findNextValue(0);
457         }
458 
459         @Override
460         public boolean hasNext() {
461             return nextNameIndex != -1;
462         }
463 
464         @Override
465         public CharSequence next() {
466             if (!hasNext()) {
467                 throw new NoSuchElementException();
468             }
469             CharSequence value = nameValuePairs[nextNameIndex + 1];
470             nextNameIndex = findNextValue(nextNameIndex + 2);
471             return value;
472         }
473 
474         @Override
475         public void remove() {
476             throw new UnsupportedOperationException("read only");
477         }
478 
479         private int findNextValue(int fromIndex) {
480             for (int i = fromIndex; i < nameValuePairs.length; i += 2) {
481                 final CharSequence roName = nameValuePairs[i];
482                 if (nameHash == AsciiString.hashCode(roName) && contentEqualsIgnoreCase(name, roName)) {
483                     return i;
484                 }
485             }
486             return -1;
487         }
488     }
489 }