Skip to content

Commit cf09c51

Browse files
committed
Add EncodingNegotiator
1 parent f221f24 commit cf09c51

File tree

4 files changed

+276
-170
lines changed

4 files changed

+276
-170
lines changed

java/org/apache/coyote/CompressionConfig.java

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -354,33 +354,7 @@ public OutputFilterFactory useCompression(Request request, Response response, Li
354354
* Negotiate the best encoding from TE header entries against available factories
355355
*/
356356
private OutputFilterFactory negotiateTE(List<OutputFilterFactory> factories, List<TE> entries) {
357-
OutputFilterFactory bestFactory = null;
358-
double bestQuality = 0;
359-
int bestServerPriority = Integer.MAX_VALUE;
360-
361-
for (int i = 0; i < factories.size(); i++) {
362-
OutputFilterFactory factory = factories.get(i);
363-
String factoryEncoding = factory.getEncodingName().toLowerCase(Locale.ENGLISH);
364-
365-
for (TE entry : entries) {
366-
String entryEncoding = entry.getEncoding().toLowerCase(Locale.ENGLISH);
367-
double quality = entry.getQuality();
368-
369-
if (quality <= 0) {
370-
continue;
371-
}
372-
373-
if (factoryEncoding.equals(entryEncoding) || "*".equals(entryEncoding)) {
374-
if (quality > bestQuality || (quality == bestQuality && i < bestServerPriority)) {
375-
bestFactory = factory;
376-
bestQuality = quality;
377-
bestServerPriority = i;
378-
}
379-
}
380-
}
381-
}
382-
383-
return bestFactory;
357+
return EncodingNegotiator.negotiateTE(factories, entries);
384358
}
385359

386360
/**
@@ -389,33 +363,7 @@ private OutputFilterFactory negotiateTE(List<OutputFilterFactory> factories, Lis
389363
private OutputFilterFactory negotiateAcceptEncoding(
390364
List<OutputFilterFactory> factories,
391365
List<AcceptEncoding> acceptEncodings) {
392-
OutputFilterFactory bestFactory = null;
393-
double bestQuality = 0;
394-
int bestServerPriority = Integer.MAX_VALUE;
395-
396-
for (int i = 0; i < factories.size(); i++) {
397-
OutputFilterFactory factory = factories.get(i);
398-
String factoryEncoding = factory.getEncodingName().toLowerCase(Locale.ENGLISH);
399-
400-
for (AcceptEncoding ae : acceptEncodings) {
401-
String aeEncoding = ae.getEncoding().toLowerCase(Locale.ENGLISH);
402-
double quality = ae.getQuality();
403-
404-
if (quality <= 0) {
405-
continue;
406-
}
407-
408-
if (factoryEncoding.equals(aeEncoding) || "*".equals(aeEncoding)) {
409-
if (quality > bestQuality || (quality == bestQuality && i < bestServerPriority)) {
410-
bestFactory = factory;
411-
bestQuality = quality;
412-
bestServerPriority = i;
413-
}
414-
}
415-
}
416-
}
417-
418-
return bestFactory;
366+
return EncodingNegotiator.negotiateAcceptEncoding(factories, acceptEncodings);
419367
}
420368

421369
/**
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.coyote;
18+
19+
import java.util.List;
20+
import java.util.Locale;
21+
import java.util.function.Function;
22+
import java.util.function.ToDoubleFunction;
23+
24+
import org.apache.coyote.http11.filters.OutputFilterFactory;
25+
import org.apache.tomcat.util.http.parser.AcceptEncoding;
26+
import org.apache.tomcat.util.http.parser.TE;
27+
28+
/**
29+
* Utility class for negotiating transfer/content encodings using TE and
30+
* Accept-Encoding headers.
31+
*/
32+
public class EncodingNegotiator {
33+
34+
/**
35+
* Selects the best {@link OutputFilterFactory} for the HTTP {@code TE} header.
36+
* <p>Rules:
37+
* - Prefer the entry with the highest quality (q) value; entries with {@code q <= 0} are ignored.
38+
* - Wildcard ({@code *}) matches any server factory.
39+
* - On quality ties, prefer the factory with the lowest server priority (its index in {@code factories}).
40+
* - Encoding name matching is case-insensitive.
41+
*
42+
* @param factories The available factories in server-priority order (index 0 is highest)
43+
* @param entries The parsed {@code TE} header entries
44+
* @return The selected factory or {@code null} if no match
45+
*/
46+
public static OutputFilterFactory negotiateTE(List<OutputFilterFactory> factories, List<TE> entries) {
47+
return negotiateEncodings(factories, entries, TE::getEncoding, TE::getQuality);
48+
}
49+
50+
/**
51+
* Selects the best {@link OutputFilterFactory} for the HTTP {@code Accept-Encoding} header.
52+
* <p>Rules:
53+
* - Prefer the entry with the highest quality (q) value; entries with {@code q <= 0} are ignored.
54+
* - Wildcard ({@code *}) matches any server factory.
55+
* - On quality ties, prefer the factory with the lowest server priority (its index in {@code factories}).
56+
* - Encoding name matching is case-insensitive.
57+
*
58+
* @param factories The available factories in server-priority order (index 0 is highest)
59+
* @param acceptEncodings The parsed {@code Accept-Encoding} entries
60+
* @return The selected factory or {@code null} if no match
61+
*/
62+
public static OutputFilterFactory negotiateAcceptEncoding(
63+
List<OutputFilterFactory> factories, List<AcceptEncoding> acceptEncodings) {
64+
return negotiateEncodings(factories, acceptEncodings, AcceptEncoding::getEncoding, AcceptEncoding::getQuality);
65+
}
66+
67+
private static <T> OutputFilterFactory negotiateEncodings(
68+
List<OutputFilterFactory> factories,
69+
List<T> entries,
70+
Function<T,String> encodingFn,
71+
ToDoubleFunction<T> qualityFn) {
72+
OutputFilterFactory bestFactory = null;
73+
double bestQuality = 0;
74+
int bestServerPriority = Integer.MAX_VALUE;
75+
76+
for (int i = 0; i < factories.size(); i++) {
77+
OutputFilterFactory factory = factories.get(i);
78+
String factoryEncoding = factory.getEncodingName().toLowerCase(Locale.ENGLISH);
79+
80+
for (T entry : entries) {
81+
String entryEncoding = encodingFn.apply(entry).toLowerCase(Locale.ENGLISH);
82+
double quality = qualityFn.applyAsDouble(entry);
83+
84+
if (quality <= 0) {
85+
continue;
86+
}
87+
88+
if (factoryEncoding.equals(entryEncoding) || "*".equals(entryEncoding)) {
89+
if (quality > bestQuality || (quality == bestQuality && i < bestServerPriority)) {
90+
bestFactory = factory;
91+
bestQuality = quality;
92+
bestServerPriority = i;
93+
}
94+
}
95+
}
96+
}
97+
98+
return bestFactory;
99+
}
100+
}

test/org/apache/coyote/TestCompressionConfig.java

Lines changed: 34 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -154,170 +154,87 @@ public void testGzipOutputFilterFactoryJavaBeanProperties() {
154154
}
155155

156156
@Test
157-
public void testNegotiationQualityFactor() {
157+
public void testCompressionOff() {
158158
CompressionConfig config = new CompressionConfig();
159-
config.setCompression("force");
160-
161-
// Create a dummy "deflate" factory for testing
162-
OutputFilterFactory deflateFactory = new OutputFilterFactory() {
163-
public OutputFilter createFilter() {
164-
return new GzipOutputFilter();
165-
}
166-
167-
public String getEncodingName() {
168-
return "deflate";
169-
}
170-
};
171-
172-
List<OutputFilterFactory> factories = new ArrayList<>();
173-
factories.add(new GzipOutputFilterFactory()); // server priority 0
174-
factories.add(deflateFactory); // server priority 1
159+
// Default compression is "off"
175160

176-
// Client prefers deflate over gzip
177161
Request request = new Request();
178162
Response response = new Response();
179-
request.getMimeHeaders().addValue("accept-encoding").setString("gzip;q=0.5, deflate;q=1.0");
180-
181-
OutputFilterFactory result = config.useCompression(request, response, factories);
182-
Assert.assertNotNull(result);
183-
Assert.assertEquals("deflate", result.getEncodingName());
184-
}
185-
186-
@Test
187-
public void testNegotiationServerPriority() {
188-
CompressionConfig config = new CompressionConfig();
189-
config.setCompression("force");
190-
191-
OutputFilterFactory brFactory = new OutputFilterFactory() {
192-
@Override
193-
public OutputFilter createFilter() {
194-
return new GzipOutputFilter();
195-
}
196-
197-
@Override
198-
public String getEncodingName() {
199-
return "br";
200-
}
201-
};
163+
request.getMimeHeaders().addValue("accept-encoding").setString("gzip");
202164

203-
// brotil first (server priority), then gzip
204165
List<OutputFilterFactory> factories = new ArrayList<>();
205-
factories.add(brFactory);
206166
factories.add(new GzipOutputFilterFactory());
207-
208-
// Client accepts both with equal quality
209-
Request request = new Request();
210-
Response response = new Response();
211-
request.getMimeHeaders().addValue("accept-encoding").setString("gzip, br");
212-
213167
OutputFilterFactory result = config.useCompression(request, response, factories);
214-
Assert.assertNotNull(result);
215-
// Server priority wins on equal quality
216-
Assert.assertEquals("br", result.getEncodingName());
168+
Assert.assertNull(result);
217169
}
218170

219171
@Test
220-
public void testNegotiationWildcard() {
172+
public void testAlreadyCompressedContentEncoding() {
221173
CompressionConfig config = new CompressionConfig();
222174
config.setCompression("force");
223175

224-
List<OutputFilterFactory> factories = new ArrayList<>();
225-
factories.add(new GzipOutputFilterFactory());
226-
227-
// Client accepts any encoding
228176
Request request = new Request();
229177
Response response = new Response();
230-
request.getMimeHeaders().addValue("accept-encoding").setString("*");
231-
232-
OutputFilterFactory result = config.useCompression(request, response, factories);
233-
Assert.assertNotNull(result);
234-
Assert.assertEquals("gzip", result.getEncodingName());
235-
}
236-
237-
@Test
238-
public void testNegotiationNoMatch() {
239-
CompressionConfig config = new CompressionConfig();
240-
config.setCompression("force");
178+
request.getMimeHeaders().addValue("accept-encoding").setString("gzip");
179+
// Response already has gzip Content-Encoding - should skip compression
180+
response.getMimeHeaders().addValue("content-encoding").setString("gzip");
241181

242182
List<OutputFilterFactory> factories = new ArrayList<>();
243183
factories.add(new GzipOutputFilterFactory());
244-
245-
// Client only accepts brotil, but server only has gzip
246-
Request request = new Request();
247-
Response response = new Response();
248-
request.getMimeHeaders().addValue("accept-encoding").setString("br");
249-
250184
OutputFilterFactory result = config.useCompression(request, response, factories);
251185
Assert.assertNull(result);
252186
}
253187

254188
@Test
255-
public void testNegotiationQZero() {
189+
public void testNegotiationViaAcceptEncoding() throws Exception {
256190
CompressionConfig config = new CompressionConfig();
257191
config.setCompression("force");
258192

193+
// Factories: gzip then deflate (server priority)
259194
List<OutputFilterFactory> factories = new ArrayList<>();
260195
factories.add(new GzipOutputFilterFactory());
196+
OutputFilterFactory deflateFactory = new OutputFilterFactory() {
197+
@Override
198+
public OutputFilter createFilter() {
199+
return new GzipOutputFilter();
200+
}
201+
@Override
202+
public String getEncodingName() {
203+
return "deflate";
204+
}
205+
};
206+
factories.add(deflateFactory);
261207

262-
// Client explicitly rejects gzip with q=0
208+
// Client prefers deflate over gzip
263209
Request request = new Request();
264210
Response response = new Response();
265-
request.getMimeHeaders().addValue("accept-encoding").setString("gzip;q=0");
211+
request.getMimeHeaders().addValue("accept-encoding").setString("deflate;q=1.0, gzip;q=0.5");
266212

267213
OutputFilterFactory result = config.useCompression(request, response, factories);
268-
Assert.assertNull(result);
269-
}
270-
271-
@Test
272-
public void testCompressionOff() {
273-
CompressionConfig config = new CompressionConfig();
274-
// Default compression is "off"
275-
276-
Request request = new Request();
277-
Response response = new Response();
278-
request.getMimeHeaders().addValue("accept-encoding").setString("gzip");
279-
280-
List<OutputFilterFactory> factories = new ArrayList<>();
281-
factories.add(new GzipOutputFilterFactory());
282-
OutputFilterFactory result = config.useCompression(request, response, factories);
283-
Assert.assertNull(result);
214+
Assert.assertNotNull(result);
215+
Assert.assertEquals("deflate", result.getEncodingName());
216+
Assert.assertEquals("deflate", response.getMimeHeaders().getHeader("Content-Encoding"));
217+
Assert.assertNull(response.getMimeHeaders().getHeader("Transfer-Encoding"));
284218
}
285219

286220
@Test
287-
public void testAlreadyCompressedContentEncoding() {
221+
public void testNegotiationViaTE() throws Exception {
288222
CompressionConfig config = new CompressionConfig();
289223
config.setCompression("force");
290224

291-
Request request = new Request();
292-
Response response = new Response();
293-
request.getMimeHeaders().addValue("accept-encoding").setString("gzip");
294-
// Response already has gzip Content-Encoding - should skip compression
295-
response.getMimeHeaders().addValue("content-encoding").setString("gzip");
296-
225+
// Factories: gzip then deflate (server priority)
297226
List<OutputFilterFactory> factories = new ArrayList<>();
298227
factories.add(new GzipOutputFilterFactory());
299-
OutputFilterFactory result = config.useCompression(request, response, factories);
300-
Assert.assertNull(result);
301-
}
302-
303-
@Test
304-
public void testTENegotiationWithFactories() {
305-
CompressionConfig config = new CompressionConfig();
306-
config.setCompression("force");
307-
308228
OutputFilterFactory deflateFactory = new OutputFilterFactory() {
309-
229+
@Override
310230
public OutputFilter createFilter() {
311231
return new GzipOutputFilter();
312232
}
313-
233+
@Override
314234
public String getEncodingName() {
315235
return "deflate";
316236
}
317237
};
318-
319-
List<OutputFilterFactory> factories = new ArrayList<>();
320-
factories.add(new GzipOutputFilterFactory());
321238
factories.add(deflateFactory);
322239

323240
// TE header should use Transfer-Encoding, not Content-Encoding
@@ -328,8 +245,9 @@ public String getEncodingName() {
328245
OutputFilterFactory result = config.useCompression(request, response, factories);
329246
Assert.assertNotNull(result);
330247
Assert.assertEquals("deflate", result.getEncodingName());
331-
// TE negotiation sets Transfer-Encoding, not Content-Encoding
332248
Assert.assertEquals("deflate", response.getMimeHeaders().getHeader("Transfer-Encoding"));
333-
Assert.assertNull(response.getMimeHeaders().getValue("Content-Encoding"));
249+
Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding"));
334250
}
251+
252+
335253
}

0 commit comments

Comments
 (0)