|
1 | 1 | package com.retailsvc.http.internal; |
2 | 2 |
|
| 3 | +import com.retailsvc.http.ContentCoding; |
| 4 | +import java.util.HashMap; |
| 5 | +import java.util.List; |
3 | 6 | import java.util.Locale; |
| 7 | +import java.util.Map; |
| 8 | +import java.util.Optional; |
4 | 9 |
|
5 | 10 | /** Parses {@code Accept-Encoding} request header values (RFC 9110 §12.5.3). */ |
6 | 11 | public final class AcceptEncodingHeader { |
7 | 12 |
|
| 13 | + private static final double DEFAULT_WEIGHT = 1.0; |
| 14 | + |
8 | 15 | private AcceptEncodingHeader() {} |
9 | 16 |
|
10 | 17 | /** |
11 | | - * Whether the client accepts a gzip-coded response. A {@code null}, blank, or unrelated header |
12 | | - * yields {@code false}. An explicit {@code gzip;q=0} is a refusal and outranks a positive |
13 | | - * wildcard; a wildcard applies only when gzip is not listed in its own right. |
| 18 | + * Chooses the coding the client weights highest among {@code candidates}, which arrive in server |
| 19 | + * preference order, so the earlier candidate wins a tie. Empty means send the body uncoded — also |
| 20 | + * the answer for an absent header, since not asking for a coding is not the same as accepting |
| 21 | + * any. |
| 22 | + * |
| 23 | + * <p>A coding the client names outranks its {@code *} entry, so {@code gzip;q=0} refuses gzip |
| 24 | + * even beside a positive wildcard. A weight of zero refuses, and a name listed twice takes its |
| 25 | + * higher weight. |
14 | 26 | */ |
15 | | - public static boolean acceptsGzip(String header) { |
16 | | - if (header == null) { |
17 | | - return false; |
| 27 | + public static Optional<ContentCoding> select(String header, List<ContentCoding> candidates) { |
| 28 | + if (header == null || candidates.isEmpty()) { |
| 29 | + return Optional.empty(); |
| 30 | + } |
| 31 | + Map<String, Double> weights = weights(header); |
| 32 | + ContentCoding best = null; |
| 33 | + double bestWeight = 0; |
| 34 | + for (ContentCoding candidate : candidates) { |
| 35 | + double weight = weightOf(candidate, weights); |
| 36 | + if (weight > bestWeight) { |
| 37 | + best = candidate; |
| 38 | + bestWeight = weight; |
| 39 | + } |
18 | 40 | } |
19 | | - boolean gzipSeen = false; |
20 | | - boolean gzipAccepted = false; |
21 | | - boolean wildcardAccepted = false; |
| 41 | + return Optional.ofNullable(best); |
| 42 | + } |
| 43 | + |
| 44 | + /** The weight the client gave each coding it listed, keeping the higher for a repeated one. */ |
| 45 | + private static Map<String, Double> weights(String header) { |
| 46 | + Map<String, Double> weights = new HashMap<>(); |
22 | 47 | for (String token : header.split(",")) { |
23 | 48 | int semi = token.indexOf(';'); |
24 | 49 | String coding = (semi < 0 ? token : token.substring(0, semi)).trim().toLowerCase(Locale.ROOT); |
25 | | - boolean accepted = positiveWeight(token); |
26 | | - if ("gzip".equals(coding) || "x-gzip".equals(coding)) { |
27 | | - gzipSeen = true; |
28 | | - gzipAccepted |= accepted; |
29 | | - } else if ("*".equals(coding)) { |
30 | | - wildcardAccepted |= accepted; |
| 50 | + if (!coding.isEmpty()) { |
| 51 | + weights.merge(coding, weight(token), Math::max); |
31 | 52 | } |
32 | 53 | } |
33 | | - return gzipSeen ? gzipAccepted : wildcardAccepted; |
| 54 | + return weights; |
| 55 | + } |
| 56 | + |
| 57 | + /** A candidate's weight from its own name or an alias, and only failing both, the wildcard. */ |
| 58 | + private static double weightOf(ContentCoding coding, Map<String, Double> weights) { |
| 59 | + Double listed = weights.get(coding.token()); |
| 60 | + for (String alias : coding.aliases()) { |
| 61 | + Double aliased = weights.get(alias); |
| 62 | + if (aliased != null && (listed == null || aliased > listed)) { |
| 63 | + listed = aliased; |
| 64 | + } |
| 65 | + } |
| 66 | + if (listed != null) { |
| 67 | + return listed; |
| 68 | + } |
| 69 | + return weights.getOrDefault("*", 0.0); |
34 | 70 | } |
35 | 71 |
|
36 | 72 | /** |
37 | | - * Whether the token's {@code q} weight admits the coding. An absent or unparsable weight reads as |
38 | | - * the default 1.0 — a malformed header should not silently disable compression. |
| 73 | + * Reads the {@code q} weight from a token. An absent or unparsable weight reads as the default |
| 74 | + * 1.0 — a malformed header should not silently disable compression. |
39 | 75 | */ |
40 | | - private static boolean positiveWeight(String token) { |
| 76 | + private static double weight(String token) { |
41 | 77 | String weight = ContentTypeHeader.parameter(token, "q").orElse(null); |
42 | 78 | if (weight == null) { |
43 | | - return true; |
| 79 | + return DEFAULT_WEIGHT; |
44 | 80 | } |
45 | 81 | try { |
46 | | - return Double.parseDouble(weight) > 0; |
| 82 | + return Double.parseDouble(weight); |
47 | 83 | } catch (NumberFormatException _) { |
48 | | - return true; |
| 84 | + return DEFAULT_WEIGHT; |
49 | 85 | } |
50 | 86 | } |
51 | 87 | } |
0 commit comments