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