1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
36
37
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
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
106 if (headers == this) {
107 throw new IllegalArgumentException("can't add to itself.");
108 }
109 if (headers instanceof CombinedHttpHeadersImpl) {
110 if (isEmpty()) {
111
112 addImpl(headers);
113 } else {
114
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
258
259
260
261 private interface CsvValueEscaper<T> {
262
263
264
265
266
267 CharSequence escape(T value);
268 }
269 }
270 }