View Javadoc
1   /*
2    * Copyright 2015 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.handler.codec.DefaultHeaders;
19  import io.netty5.handler.codec.Headers;
20  import io.netty5.handler.codec.ValueConverter;
21  import io.netty5.util.HashingStrategy;
22  import io.netty5.util.internal.StringUtil;
23  
24  import java.util.Collection;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  import static io.netty5.handler.codec.http.HttpHeaderNames.SET_COOKIE;
30  import static io.netty5.util.AsciiString.CASE_INSENSITIVE_HASHER;
31  import static io.netty5.util.internal.StringUtil.COMMA;
32  import static io.netty5.util.internal.StringUtil.unescapeCsvFields;
33  
34  /**
35   * Will add multiple values for the same header as single header with a comma separated list of values.
36   * <p>
37   * Please refer to section <a href="https://tools.ietf.org/html/rfc7230#section-3.2.2">RFC 7230, 3.2.2</a>.
38   */
39  public class CombinedHttpHeaders extends DefaultHttpHeaders {
40      public CombinedHttpHeaders(boolean validate) {
41          super(new CombinedHttpHeadersImpl(CASE_INSENSITIVE_HASHER, valueConverter(validate), nameValidator(validate)));
42      }
43  
44      @Override
45      public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
46          return super.containsValue(name, StringUtil.trimOws(value), ignoreCase);
47      }
48  
49      private static final class CombinedHttpHeadersImpl
50              extends DefaultHeaders<CharSequence, CharSequence, CombinedHttpHeadersImpl> {
51          /**
52           * An estimate of the size of a header value.
53           */
54          private static final int VALUE_LENGTH_ESTIMATE = 10;
55          private CsvValueEscaper<Object> objectEscaper;
56          private CsvValueEscaper<CharSequence> charSequenceEscaper;
57  
58          private CsvValueEscaper<Object> objectEscaper() {
59              if (objectEscaper == null) {
60                  objectEscaper = value -> StringUtil.escapeCsv(valueConverter().convertObject(value), true);
61              }
62              return objectEscaper;
63          }
64  
65          private CsvValueEscaper<CharSequence> charSequenceEscaper() {
66              if (charSequenceEscaper == null) {
67                  charSequenceEscaper = value -> StringUtil.escapeCsv(value, true);
68              }
69              return charSequenceEscaper;
70          }
71  
72          CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy,
73                  ValueConverter<CharSequence> valueConverter,
74                  io.netty5.handler.codec.DefaultHeaders.NameValidator<CharSequence> nameValidator) {
75              super(nameHashingStrategy, valueConverter, nameValidator);
76          }
77  
78          @Override
79          public Iterator<CharSequence> valueIterator(CharSequence name) {
80              Iterator<CharSequence> itr = super.valueIterator(name);
81              if (!itr.hasNext() || cannotBeCombined(name)) {
82                  return itr;
83              }
84              Iterator<CharSequence> unescapedItr = unescapeCsvFields(itr.next()).iterator();
85              if (itr.hasNext()) {
86                  throw new IllegalStateException("CombinedHttpHeaders should only have one value");
87              }
88              return unescapedItr;
89          }
90  
91          @Override
92          public List<CharSequence> getAll(CharSequence name) {
93              List<CharSequence> values = super.getAll(name);
94              if (values.isEmpty() || cannotBeCombined(name)) {
95                  return values;
96              }
97              if (values.size() != 1) {
98                  throw new IllegalStateException("CombinedHttpHeaders should only have one value");
99              }
100             return unescapeCsvFields(values.get(0));
101         }
102 
103         @Override
104         public CombinedHttpHeadersImpl add(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
105             // Override the fast-copy mechanism used by DefaultHeaders
106             if (headers == this) {
107                 throw new IllegalArgumentException("can't add to itself.");
108             }
109             if (headers instanceof CombinedHttpHeadersImpl) {
110                 if (isEmpty()) {
111                     // Can use the fast underlying copy
112                     addImpl(headers);
113                 } else {
114                     // Values are already escaped so don't escape again
115                     for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
116                         addEscapedValue(header.getKey(), header.getValue());
117                     }
118                 }
119             } else {
120                 for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
121                     add(header.getKey(), header.getValue());
122                 }
123             }
124             return this;
125         }
126 
127         @Override
128         public CombinedHttpHeadersImpl set(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
129             if (headers == this) {
130                 return this;
131             }
132             clear();
133             return add(headers);
134         }
135 
136         @Override
137         public CombinedHttpHeadersImpl setAll(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
138             if (headers == this) {
139                 return this;
140             }
141             for (CharSequence key : headers.names()) {
142                 remove(key);
143             }
144             return add(headers);
145         }
146 
147         @Override
148         public CombinedHttpHeadersImpl add(CharSequence name, CharSequence value) {
149             return addEscapedValue(name, charSequenceEscaper().escape(value));
150         }
151 
152         @Override
153         public CombinedHttpHeadersImpl add(CharSequence name, CharSequence... values) {
154             return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
155         }
156 
157         @Override
158         public CombinedHttpHeadersImpl add(CharSequence name, Iterable<? extends CharSequence> values) {
159             return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
160         }
161 
162         @Override
163         public CombinedHttpHeadersImpl addObject(CharSequence name, Object value) {
164             return addEscapedValue(name, commaSeparate(objectEscaper(), value));
165         }
166 
167         @Override
168         public CombinedHttpHeadersImpl addObject(CharSequence name, Iterable<?> values) {
169             return addEscapedValue(name, commaSeparate(objectEscaper(), values));
170         }
171 
172         @Override
173         public CombinedHttpHeadersImpl addObject(CharSequence name, Object... values) {
174             return addEscapedValue(name, commaSeparate(objectEscaper(), values));
175         }
176 
177         @Override
178         public CombinedHttpHeadersImpl set(CharSequence name, CharSequence... values) {
179             super.set(name, commaSeparate(charSequenceEscaper(), values));
180             return this;
181         }
182 
183         @Override
184         public CombinedHttpHeadersImpl set(CharSequence name, Iterable<? extends CharSequence> values) {
185             super.set(name, commaSeparate(charSequenceEscaper(), values));
186             return this;
187         }
188 
189         @Override
190         public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) {
191             super.set(name, commaSeparate(objectEscaper(), value));
192             return this;
193         }
194 
195         @Override
196         public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
197             super.set(name, commaSeparate(objectEscaper(), values));
198             return this;
199         }
200 
201         @Override
202         public CombinedHttpHeadersImpl setObject(CharSequence name, Iterable<?> values) {
203             super.set(name, commaSeparate(objectEscaper(), values));
204             return this;
205         }
206 
207         private static boolean cannotBeCombined(CharSequence name) {
208             return SET_COOKIE.contentEqualsIgnoreCase(name);
209         }
210 
211         private CombinedHttpHeadersImpl addEscapedValue(CharSequence name, CharSequence escapedValue) {
212             CharSequence currentValue = super.get(name);
213             if (currentValue == null || cannotBeCombined(name)) {
214                 super.add(name, escapedValue);
215             } else {
216                 super.set(name, commaSeparateEscapedValues(currentValue, escapedValue));
217             }
218             return this;
219         }
220 
221         private static <T> CharSequence commaSeparate(CsvValueEscaper<T> escaper, T... values) {
222             StringBuilder sb = new StringBuilder(values.length * VALUE_LENGTH_ESTIMATE);
223             if (values.length > 0) {
224                 int end = values.length - 1;
225                 for (int i = 0; i < end; i++) {
226                     sb.append(escaper.escape(values[i])).append(COMMA);
227                 }
228                 sb.append(escaper.escape(values[end]));
229             }
230             return sb;
231         }
232 
233         private static <T> CharSequence commaSeparate(CsvValueEscaper<T> escaper, Iterable<? extends T> values) {
234             @SuppressWarnings("rawtypes")
235             final StringBuilder sb = values instanceof Collection
236                     ? new StringBuilder(((Collection) values).size() * VALUE_LENGTH_ESTIMATE) : new StringBuilder();
237             Iterator<? extends T> iterator = values.iterator();
238             if (iterator.hasNext()) {
239                 T next = iterator.next();
240                 while (iterator.hasNext()) {
241                     sb.append(escaper.escape(next)).append(COMMA);
242                     next = iterator.next();
243                 }
244                 sb.append(escaper.escape(next));
245             }
246             return sb;
247         }
248 
249         private static CharSequence commaSeparateEscapedValues(CharSequence currentValue, CharSequence value) {
250             return new StringBuilder(currentValue.length() + 1 + value.length())
251                     .append(currentValue)
252                     .append(COMMA)
253                     .append(value);
254         }
255 
256         /**
257          * Escapes comma separated values (CSV).
258          *
259          * @param <T> The type that a concrete implementation handles
260          */
261         private interface CsvValueEscaper<T> {
262             /**
263              * Appends the value to the specified {@link StringBuilder}, escaping if necessary.
264              *
265              * @param value the value to be appended, escaped if necessary
266              */
267             CharSequence escape(T value);
268         }
269     }
270 }