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.netty5.handler.codec.http;
17  
18  import io.netty5.util.AsciiString;
19  import io.netty5.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.netty5.handler.codec.CharSequenceValueConverter.INSTANCE;
32  import static io.netty5.handler.codec.http.DefaultHttpHeaders.HttpNameValidator;
33  import static io.netty5.util.AsciiString.contentEquals;
34  import static io.netty5.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         if (value == null) {
122             return null;
123         }
124         try {
125             return INSTANCE.convertToTimeMillis(value);
126         } catch (RuntimeException e) {
127             return null;
128         }
129     }
130 
131     @Override
132     public long getTimeMillis(CharSequence name, long defaultValue) {
133         CharSequence value = get0(name);
134         if (value == null) {
135             return defaultValue;
136         }
137         try {
138             return INSTANCE.convertToTimeMillis(value);
139         } catch (RuntimeException e) {
140             return defaultValue;
141         }
142     }
143 
144     @Override
145     public List<String> getAll(String name) {
146         if (isEmpty()) {
147             return Collections.emptyList();
148         }
149         final int nameHash = AsciiString.hashCode(name);
150         List<String> values = new ArrayList<>(4);
151         for (int i = 0; i < nameValuePairs.length; i += 2) {
152             CharSequence roName = nameValuePairs[i];
153             if (AsciiString.hashCode(roName) == nameHash && contentEqualsIgnoreCase(roName, name)) {
154                 values.add(nameValuePairs[i + 1].toString()); // lgtm[java/index-out-of-bounds]
155             }
156         }
157         return values;
158     }
159 
160     @Override
161     public List<Map.Entry<String, String>> entries() {
162         if (isEmpty()) {
163             return Collections.emptyList();
164         }
165         List<Map.Entry<String, String>> entries = new ArrayList<>(size());
166         for (int i = 0; i < nameValuePairs.length; i += 2) {
167             entries.add(new SimpleImmutableEntry<>(nameValuePairs[i].toString(),
168                     nameValuePairs[i + 1].toString())); // lgtm[java/index-out-of-bounds]
169         }
170         return entries;
171     }
172 
173     @Override
174     public boolean contains(String name) {
175         return get0(name) != null;
176     }
177 
178     @Override
179     public boolean contains(String name, String value, boolean ignoreCase) {
180         return containsValue(name, value, ignoreCase);
181     }
182 
183     @Override
184     public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
185         if (ignoreCase) {
186             for (int i = 0; i < nameValuePairs.length; i += 2) {
187                 if (contentEqualsIgnoreCase(nameValuePairs[i], name) &&
188                         contentEqualsIgnoreCase(nameValuePairs[i + 1], value)) { // lgtm[java/index-out-of-bounds]
189                     return true;
190                 }
191             }
192         } else {
193             for (int i = 0; i < nameValuePairs.length; i += 2) {
194                 if (contentEqualsIgnoreCase(nameValuePairs[i], name) &&
195                         contentEquals(nameValuePairs[i + 1], value)) { // lgtm[java/index-out-of-bounds]
196                     return true;
197                 }
198             }
199         }
200         return false;
201     }
202 
203     @Override
204     public Iterator<String> valueStringIterator(CharSequence name) {
205         return new ReadOnlyStringValueIterator(name);
206     }
207 
208     @Override
209     public Iterator<CharSequence> valueCharSequenceIterator(CharSequence name) {
210         return new ReadOnlyValueIterator(name);
211     }
212 
213     @Override
214     public Iterator<Map.Entry<String, String>> iterator() {
215         return new ReadOnlyStringIterator();
216     }
217 
218     @Override
219     public Iterator<Map.Entry<CharSequence, CharSequence>> iteratorCharSequence() {
220         return new ReadOnlyIterator();
221     }
222 
223     @Override
224     public boolean isEmpty() {
225         return nameValuePairs.length == 0;
226     }
227 
228     @Override
229     public int size() {
230         return nameValuePairs.length >>> 1;
231     }
232 
233     @Override
234     public Set<String> names() {
235         if (isEmpty()) {
236             return Collections.emptySet();
237         }
238         Set<String> names = new LinkedHashSet<>(size());
239         for (int i = 0; i < nameValuePairs.length; i += 2) {
240             names.add(nameValuePairs[i].toString());
241         }
242         return names;
243     }
244 
245     @Override
246     public HttpHeaders add(String name, Object value) {
247         throw new UnsupportedOperationException("read only");
248     }
249 
250     @Override
251     public HttpHeaders add(String name, Iterable<?> values) {
252         throw new UnsupportedOperationException("read only");
253     }
254 
255     @Override
256     public HttpHeaders addInt(CharSequence name, int value) {
257         throw new UnsupportedOperationException("read only");
258     }
259 
260     @Override
261     public HttpHeaders addShort(CharSequence name, short value) {
262         throw new UnsupportedOperationException("read only");
263     }
264 
265     @Override
266     public HttpHeaders set(String name, Object value) {
267         throw new UnsupportedOperationException("read only");
268     }
269 
270     @Override
271     public HttpHeaders set(String name, Iterable<?> values) {
272         throw new UnsupportedOperationException("read only");
273     }
274 
275     @Override
276     public HttpHeaders setInt(CharSequence name, int value) {
277         throw new UnsupportedOperationException("read only");
278     }
279 
280     @Override
281     public HttpHeaders setShort(CharSequence name, short value) {
282         throw new UnsupportedOperationException("read only");
283     }
284 
285     @Override
286     public HttpHeaders remove(String name) {
287         throw new UnsupportedOperationException("read only");
288     }
289 
290     @Override
291     public HttpHeaders clear() {
292         throw new UnsupportedOperationException("read only");
293     }
294 
295     private final class ReadOnlyIterator implements Map.Entry<CharSequence, CharSequence>,
296             Iterator<Map.Entry<CharSequence, CharSequence>> {
297         private CharSequence key;
298         private CharSequence value;
299         private int nextNameIndex;
300 
301         @Override
302         public boolean hasNext() {
303             return nextNameIndex != nameValuePairs.length;
304         }
305 
306         @Override
307         public Map.Entry<CharSequence, CharSequence> next() {
308             if (!hasNext()) {
309                 throw new NoSuchElementException();
310             }
311             key = nameValuePairs[nextNameIndex];
312             value = nameValuePairs[nextNameIndex + 1];
313             nextNameIndex += 2;
314             return this;
315         }
316 
317         @Override
318         public void remove() {
319             throw new UnsupportedOperationException("read only");
320         }
321 
322         @Override
323         public CharSequence getKey() {
324             return key;
325         }
326 
327         @Override
328         public CharSequence getValue() {
329             return value;
330         }
331 
332         @Override
333         public CharSequence setValue(CharSequence value) {
334             throw new UnsupportedOperationException("read only");
335         }
336 
337         @Override
338         public String toString() {
339             return key.toString() + '=' + value.toString();
340         }
341     }
342 
343     private final class ReadOnlyStringIterator implements Map.Entry<String, String>,
344             Iterator<Map.Entry<String, String>> {
345         private String key;
346         private String value;
347         private int nextNameIndex;
348 
349         @Override
350         public boolean hasNext() {
351             return nextNameIndex != nameValuePairs.length;
352         }
353 
354         @Override
355         public Map.Entry<String, String> next() {
356             if (!hasNext()) {
357                 throw new NoSuchElementException();
358             }
359             key = nameValuePairs[nextNameIndex].toString();
360             value = nameValuePairs[nextNameIndex + 1].toString();
361             nextNameIndex += 2;
362             return this;
363         }
364 
365         @Override
366         public void remove() {
367             throw new UnsupportedOperationException("read only");
368         }
369 
370         @Override
371         public String getKey() {
372             return key;
373         }
374 
375         @Override
376         public String getValue() {
377             return value;
378         }
379 
380         @Override
381         public String setValue(String value) {
382             throw new UnsupportedOperationException("read only");
383         }
384 
385         @Override
386         public String toString() {
387             return key + '=' + value;
388         }
389     }
390 
391     private final class ReadOnlyStringValueIterator implements Iterator<String> {
392         private final CharSequence name;
393         private final int nameHash;
394         private int nextNameIndex;
395 
396         ReadOnlyStringValueIterator(CharSequence name) {
397             this.name = name;
398             nameHash = AsciiString.hashCode(name);
399             nextNameIndex = findNextValue();
400         }
401 
402         @Override
403         public boolean hasNext() {
404             return nextNameIndex != -1;
405         }
406 
407         @Override
408         public String next() {
409             if (!hasNext()) {
410                 throw new NoSuchElementException();
411             }
412             String value = nameValuePairs[nextNameIndex + 1].toString();
413             nextNameIndex = findNextValue();
414             return value;
415         }
416 
417         @Override
418         public void remove() {
419             throw new UnsupportedOperationException("read only");
420         }
421 
422         private int findNextValue() {
423             for (int i = nextNameIndex; i < nameValuePairs.length; i += 2) {
424                 final CharSequence roName = nameValuePairs[i];
425                 if (nameHash == AsciiString.hashCode(roName) && contentEqualsIgnoreCase(name, roName)) {
426                     return i;
427                 }
428             }
429             return -1;
430         }
431     }
432 
433     private final class ReadOnlyValueIterator implements Iterator<CharSequence> {
434         private final CharSequence name;
435         private final int nameHash;
436         private int nextNameIndex;
437 
438         ReadOnlyValueIterator(CharSequence name) {
439             this.name = name;
440             nameHash = AsciiString.hashCode(name);
441             nextNameIndex = findNextValue();
442         }
443 
444         @Override
445         public boolean hasNext() {
446             return nextNameIndex != -1;
447         }
448 
449         @Override
450         public CharSequence next() {
451             if (!hasNext()) {
452                 throw new NoSuchElementException();
453             }
454             CharSequence value = nameValuePairs[nextNameIndex + 1];
455             nextNameIndex = findNextValue();
456             return value;
457         }
458 
459         @Override
460         public void remove() {
461             throw new UnsupportedOperationException("read only");
462         }
463 
464         private int findNextValue() {
465             for (int i = nextNameIndex; i < nameValuePairs.length; i += 2) {
466                 final CharSequence roName = nameValuePairs[i];
467                 if (nameHash == AsciiString.hashCode(roName) && contentEqualsIgnoreCase(name, roName)) {
468                     return i;
469                 }
470             }
471             return -1;
472         }
473     }
474 }