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