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