This is an automated email from the ASF dual-hosted git repository.
joerghoh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git
The following commit(s) were added to refs/heads/master by this push:
new a9566e2 SLING-13416 improve handling of the accept request header
(#88)
a9566e2 is described below
commit a9566e2263ac83f2fcd00081ddfe65ca7aaa5ea8
Author: Jörg Hoh <[email protected]>
AuthorDate: Wed Nov 11 15:29:25 2026 +0100
SLING-13416 improve handling of the accept request header (#88)
* SLING-13416 improve handling of the accept request header
---
.../api/request/header/JakartaMediaRangeList.java | 115 +++++++++++++++++----
.../sling/api/request/header/MediaRangeList.java | 115 +++++++++++++++++----
.../sling/api/request/header/package-info.java | 2 +-
.../request/header/JakartaMediaRangeListTest.java | 103 ++++++++++++++++++
.../api/request/header/MediaRangeListTest.java | 100 ++++++++++++++++++
5 files changed, 396 insertions(+), 39 deletions(-)
diff --git
a/src/main/java/org/apache/sling/api/request/header/JakartaMediaRangeList.java
b/src/main/java/org/apache/sling/api/request/header/JakartaMediaRangeList.java
index bc7bf5d..4a98646 100644
---
a/src/main/java/org/apache/sling/api/request/header/JakartaMediaRangeList.java
+++
b/src/main/java/org/apache/sling/api/request/header/JakartaMediaRangeList.java
@@ -19,12 +19,17 @@
package org.apache.sling.api.request.header;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import jakarta.servlet.http.HttpServletRequest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ConsumerType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -123,20 +128,36 @@ public class JakartaMediaRangeList extends
TreeSet<JakartaMediaRangeList.MediaRa
/**
* Given a list of media types, returns the one is preferred by this
<code>MediaRangeList</code>.
+ * Wildcards are matched against the offered types. The most specific
matching range,
+ * including media parameters before {@code q}, determines each type's
quality.
+ * Types with quality zero are not acceptable. Accept extensions after
{@code q} do not
+ * constrain matching. Equal-quality types follow the ordering of this
list; when the
+ * same range matches multiple offered types, their iteration order breaks
the tie.
* @param mediaRanges An array of possible {@link
JakartaMediaRangeList.MediaRange}s
* @return One of the <code>mediaRanges</code> that this
<code>MediaRangeList</code> prefers;
* or <code>null</code> if this <code>MediaRangeList</code> does not
contain any of the <code>mediaRanges</code>
* @throws NullPointerException if <code>mediaRanges</code> is
<code>null</code> or contains a <code>null</code> value
*/
- public MediaRange prefer(Set<MediaRange> mediaRanges) {
- for (MediaRange range : this) {
- for (MediaRange mediaType : mediaRanges) {
- if (range.equals(mediaType)) {
- return mediaType;
+ public @Nullable MediaRange prefer(@NotNull Set<MediaRange> mediaRanges) {
+ MediaRange preferred = null;
+ MediaRange preferredRange = null;
+ for (MediaRange mediaType : mediaRanges) {
+ Objects.requireNonNull(mediaType);
+ MediaRange matchingRange = null;
+ for (MediaRange range : this) {
+ if (range.matches(mediaType)
+ && (matchingRange == null ||
range.compareSpecificity(matchingRange) > 0)) {
+ matchingRange = range;
}
}
+ if (matchingRange != null
+ && matchingRange.getQ() > 0
+ && (preferredRange == null ||
matchingRange.compareTo(preferredRange) < 0)) {
+ preferred = mediaType;
+ preferredRange = matchingRange;
+ }
}
- return null;
+ return preferred;
}
/**
@@ -144,11 +165,12 @@ public class JakartaMediaRangeList extends
TreeSet<JakartaMediaRangeList.MediaRa
* @param mediaRanges String representations of <code>MediaRange</code>s.
The strings must be
* on the form required by {@link MediaRange#MediaRange(String)}
* @see #prefer(java.util.Set)
+ * @implNote Offered order breaks ties when multiple types match the same
range.
* @return the <code>toString()</code> representation of the preferred
<code>MediaRange</code>, or <code>null</code>
* if this <code>MediaRangeList</code> does not contain any of the
<code>mediaRanges</code>
*/
- public String prefer(String... mediaRanges) {
- Set<MediaRange> ranges = new HashSet<MediaRange>();
+ public @Nullable String prefer(@NotNull String... mediaRanges) {
+ Set<MediaRange> ranges = new LinkedHashSet<MediaRange>();
for (String mediaRange : mediaRanges) {
ranges.add(new MediaRange(mediaRange));
}
@@ -197,18 +219,24 @@ public class JakartaMediaRangeList extends
TreeSet<JakartaMediaRangeList.MediaRa
String[] parts = exp.split(";");
this.setType(parts[0].trim());
if (parts.length > 1) {
- this.parameters = new HashMap<String, String>(parts.length -
1);
+ this.parameters = new LinkedHashMap<String,
String>(parts.length - 1);
}
for (int i = 1, partsLength = parts.length; i < partsLength; i++) {
- String parameter = parts[i];
- String[] keyValue = parameter.split("=");
- if (keyValue[0].equals("q")) {
- this.q = Double.parseDouble(keyValue[1]);
- if (this.q < 0 || this.q > 1) {
+ String[] keyValue = parts[i].split("=", 2);
+ if (keyValue.length != 2
+ || keyValue[0].trim().isEmpty()
+ || keyValue[1].trim().isEmpty()) {
+ throw new IllegalArgumentException("Invalid media range
parameter: " + exp);
+ }
+ String name = keyValue[0].trim().toLowerCase(Locale.ROOT);
+ String value = keyValue[1].trim();
+ if (name.equals("q")) {
+ this.q = Double.parseDouble(value);
+ if (!Double.isFinite(this.q) || this.q < 0 || this.q > 1) {
throw new IllegalArgumentException("Quality factor out
of bounds: " + exp);
}
}
- this.parameters.put(keyValue[0], keyValue[1]);
+ this.parameters.put(name, value);
}
}
@@ -253,9 +281,14 @@ public class JakartaMediaRangeList extends
TreeSet<JakartaMediaRangeList.MediaRa
this.q = q;
}
- public String getParameter(String key) {
+ /**
+ * Returns a parameter value using a case-insensitive name lookup.
+ * @param key The parameter name, or {@code null}
+ * @return The parameter value, or {@code null} if no parameter matches
+ */
+ public @Nullable String getParameter(@Nullable String key) {
if (parameters != null) {
- return parameters.get(key);
+ return parameters.get(key == null ? null :
key.toLowerCase(Locale.ROOT));
} else {
return null;
}
@@ -277,10 +310,54 @@ public class JakartaMediaRangeList extends
TreeSet<JakartaMediaRangeList.MediaRa
return q;
}
- public Map<String, String> getParameters() {
+ /**
+ * Returns the parameters with parsed names normalized to lowercase.
+ * Parameter values retain their original casing.
+ * @return The parameter map, or an empty map if there are no
parameters
+ */
+ public @NotNull Map<String, String> getParameters() {
return parameters != null ? parameters : new HashMap<String,
String>(0);
}
+ private boolean matches(MediaRange mediaType) {
+ if ((!WILDCARD.equals(supertype) &&
!supertype.equalsIgnoreCase(mediaType.supertype))
+ || (!WILDCARD.equals(subtype) &&
!subtype.equalsIgnoreCase(mediaType.subtype))) {
+ return false;
+ }
+ if (parameters != null) {
+ for (Map.Entry<String, String> parameter :
parameters.entrySet()) {
+ if ("q".equals(parameter.getKey())) {
+ break;
+ }
+ if
(!parameter.getValue().equals(mediaType.getParameter(parameter.getKey()))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private int compareSpecificity(MediaRange other) {
+ int typeSpecificity = matchesAll() ? 0 : WILDCARD.equals(subtype)
? 1 : 2;
+ int otherTypeSpecificity = other.matchesAll() ? 0 :
WILDCARD.equals(other.subtype) ? 1 : 2;
+ return typeSpecificity != otherTypeSpecificity
+ ? Integer.compare(typeSpecificity, otherTypeSpecificity)
+ : Integer.compare(mediaParameterCount(),
other.mediaParameterCount());
+ }
+
+ private int mediaParameterCount() {
+ int count = 0;
+ if (parameters != null) {
+ for (String name : parameters.keySet()) {
+ if ("q".equals(name)) {
+ break;
+ }
+ count++;
+ }
+ }
+ return count;
+ }
+
/* -- Comparable implementation -- */
public int compareTo(MediaRange o) {
double diff = this.q - o.getQ();
diff --git
a/src/main/java/org/apache/sling/api/request/header/MediaRangeList.java
b/src/main/java/org/apache/sling/api/request/header/MediaRangeList.java
index ea90646..b626521 100644
--- a/src/main/java/org/apache/sling/api/request/header/MediaRangeList.java
+++ b/src/main/java/org/apache/sling/api/request/header/MediaRangeList.java
@@ -21,11 +21,16 @@ package org.apache.sling.api.request.header;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ConsumerType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -125,20 +130,36 @@ public class MediaRangeList extends
TreeSet<MediaRangeList.MediaRange> {
/**
* Given a list of media types, returns the one is preferred by this
<code>MediaRangeList</code>.
+ * Wildcards are matched against the offered types. The most specific
matching range,
+ * including media parameters before {@code q}, determines each type's
quality.
+ * Types with quality zero are not acceptable. Accept extensions after
{@code q} do not
+ * constrain matching. Equal-quality types follow the ordering of this
list; when the
+ * same range matches multiple offered types, their iteration order breaks
the tie.
* @param mediaRanges An array of possible {@link
MediaRangeList.MediaRange}s
* @return One of the <code>mediaRanges</code> that this
<code>MediaRangeList</code> prefers;
* or <code>null</code> if this <code>MediaRangeList</code> does not
contain any of the <code>mediaRanges</code>
* @throws NullPointerException if <code>mediaRanges</code> is
<code>null</code> or contains a <code>null</code> value
*/
- public MediaRange prefer(Set<MediaRange> mediaRanges) {
- for (MediaRange range : this) {
- for (MediaRange mediaType : mediaRanges) {
- if (range.equals(mediaType)) {
- return mediaType;
+ public @Nullable MediaRange prefer(@NotNull Set<MediaRange> mediaRanges) {
+ MediaRange preferred = null;
+ MediaRange preferredRange = null;
+ for (MediaRange mediaType : mediaRanges) {
+ Objects.requireNonNull(mediaType);
+ MediaRange matchingRange = null;
+ for (MediaRange range : this) {
+ if (range.matches(mediaType)
+ && (matchingRange == null ||
range.compareSpecificity(matchingRange) > 0)) {
+ matchingRange = range;
}
}
+ if (matchingRange != null
+ && matchingRange.getQ() > 0
+ && (preferredRange == null ||
matchingRange.compareTo(preferredRange) < 0)) {
+ preferred = mediaType;
+ preferredRange = matchingRange;
+ }
}
- return null;
+ return preferred;
}
/**
@@ -146,11 +167,12 @@ public class MediaRangeList extends
TreeSet<MediaRangeList.MediaRange> {
* @param mediaRanges String representations of <code>MediaRange</code>s.
The strings must be
* on the form required by {@link MediaRange#MediaRange(String)}
* @see #prefer(java.util.Set)
+ * @implNote Offered order breaks ties when multiple types match the same
range.
* @return the <code>toString()</code> representation of the preferred
<code>MediaRange</code>, or <code>null</code>
* if this <code>MediaRangeList</code> does not contain any of the
<code>mediaRanges</code>
*/
- public String prefer(String... mediaRanges) {
- Set<MediaRange> ranges = new HashSet<MediaRange>();
+ public @Nullable String prefer(@NotNull String... mediaRanges) {
+ Set<MediaRange> ranges = new LinkedHashSet<MediaRange>();
for (String mediaRange : mediaRanges) {
ranges.add(new MediaRange(mediaRange));
}
@@ -199,18 +221,24 @@ public class MediaRangeList extends
TreeSet<MediaRangeList.MediaRange> {
String[] parts = exp.split(";");
this.setType(parts[0].trim());
if (parts.length > 1) {
- this.parameters = new HashMap<String, String>(parts.length -
1);
+ this.parameters = new LinkedHashMap<String,
String>(parts.length - 1);
}
for (int i = 1, partsLength = parts.length; i < partsLength; i++) {
- String parameter = parts[i];
- String[] keyValue = parameter.split("=");
- if (keyValue[0].equals("q")) {
- this.q = Double.parseDouble(keyValue[1]);
- if (this.q < 0 || this.q > 1) {
+ String[] keyValue = parts[i].split("=", 2);
+ if (keyValue.length != 2
+ || keyValue[0].trim().isEmpty()
+ || keyValue[1].trim().isEmpty()) {
+ throw new IllegalArgumentException("Invalid media range
parameter: " + exp);
+ }
+ String name = keyValue[0].trim().toLowerCase(Locale.ROOT);
+ String value = keyValue[1].trim();
+ if (name.equals("q")) {
+ this.q = Double.parseDouble(value);
+ if (!Double.isFinite(this.q) || this.q < 0 || this.q > 1) {
throw new IllegalArgumentException("Quality factor out
of bounds: " + exp);
}
}
- this.parameters.put(keyValue[0], keyValue[1]);
+ this.parameters.put(name, value);
}
}
@@ -255,9 +283,14 @@ public class MediaRangeList extends
TreeSet<MediaRangeList.MediaRange> {
this.q = q;
}
- public String getParameter(String key) {
+ /**
+ * Returns a parameter value using a case-insensitive name lookup.
+ * @param key The parameter name, or {@code null}
+ * @return The parameter value, or {@code null} if no parameter matches
+ */
+ public @Nullable String getParameter(@Nullable String key) {
if (parameters != null) {
- return parameters.get(key);
+ return parameters.get(key == null ? null :
key.toLowerCase(Locale.ROOT));
} else {
return null;
}
@@ -279,10 +312,54 @@ public class MediaRangeList extends
TreeSet<MediaRangeList.MediaRange> {
return q;
}
- public Map<String, String> getParameters() {
+ /**
+ * Returns the parameters with parsed names normalized to lowercase.
+ * Parameter values retain their original casing.
+ * @return The parameter map, or an empty map if there are no
parameters
+ */
+ public @NotNull Map<String, String> getParameters() {
return parameters != null ? parameters : new HashMap<String,
String>(0);
}
+ private boolean matches(MediaRange mediaType) {
+ if ((!WILDCARD.equals(supertype) &&
!supertype.equalsIgnoreCase(mediaType.supertype))
+ || (!WILDCARD.equals(subtype) &&
!subtype.equalsIgnoreCase(mediaType.subtype))) {
+ return false;
+ }
+ if (parameters != null) {
+ for (Map.Entry<String, String> parameter :
parameters.entrySet()) {
+ if ("q".equals(parameter.getKey())) {
+ break;
+ }
+ if
(!parameter.getValue().equals(mediaType.getParameter(parameter.getKey()))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private int compareSpecificity(MediaRange other) {
+ int typeSpecificity = matchesAll() ? 0 : WILDCARD.equals(subtype)
? 1 : 2;
+ int otherTypeSpecificity = other.matchesAll() ? 0 :
WILDCARD.equals(other.subtype) ? 1 : 2;
+ return typeSpecificity != otherTypeSpecificity
+ ? Integer.compare(typeSpecificity, otherTypeSpecificity)
+ : Integer.compare(mediaParameterCount(),
other.mediaParameterCount());
+ }
+
+ private int mediaParameterCount() {
+ int count = 0;
+ if (parameters != null) {
+ for (String name : parameters.keySet()) {
+ if ("q".equals(name)) {
+ break;
+ }
+ count++;
+ }
+ }
+ return count;
+ }
+
/* -- Comparable implementation -- */
public int compareTo(MediaRange o) {
double diff = this.q - o.getQ();
diff --git
a/src/main/java/org/apache/sling/api/request/header/package-info.java
b/src/main/java/org/apache/sling/api/request/header/package-info.java
index a7085e2..24cb520 100644
--- a/src/main/java/org/apache/sling/api/request/header/package-info.java
+++ b/src/main/java/org/apache/sling/api/request/header/package-info.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-@Version("1.1.0")
+@Version("1.1.1")
package org.apache.sling.api.request.header;
import org.osgi.annotation.versioning.Version;
diff --git
a/src/test/java/org/apache/sling/api/request/header/JakartaMediaRangeListTest.java
b/src/test/java/org/apache/sling/api/request/header/JakartaMediaRangeListTest.java
index c30034f..6557641 100644
---
a/src/test/java/org/apache/sling/api/request/header/JakartaMediaRangeListTest.java
+++
b/src/test/java/org/apache/sling/api/request/header/JakartaMediaRangeListTest.java
@@ -18,6 +18,9 @@
*/
package org.apache.sling.api.request.header;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
import junit.framework.TestCase;
import org.apache.sling.api.SlingJakartaHttpServletRequest;
import org.mockito.Mockito;
@@ -46,6 +49,106 @@ public class JakartaMediaRangeListTest extends TestCase {
assertEquals("application/json", rangeList.prefer("text/html",
"application/json"));
}
+ /**
+ * Matches wildcard ranges and treats missing or empty Accept headers as
accepting all types.
+ */
+ public void testPreferWildcardsAndMissingHeader() {
+ for (String accept : new String[] {null, "", "*/*", "text/*"}) {
+ assertEquals(
+ accept, "text/html", new
JakartaMediaRangeList(accept).prefer("text/html", "application/json"));
+ }
+ assertEquals(
+ "application/json", new
JakartaMediaRangeList("application/*").prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Rejects unsupported types and q=0 ranges, including exclusions that
override broader wildcards.
+ */
+ public void testPreferExcludesUnacceptableTypes() {
+ for (String accept : new String[] {
+ "text/plain",
+ "*/*;q=0",
+ "text/html;q=0",
+ "text/html;q=0,application/json;q=0,*/*",
+ "text/*;q=0,application/*;q=0,*/*"
+ }) {
+ assertNull(accept, new
JakartaMediaRangeList(accept).prefer("text/html", "application/json"));
+ }
+ assertEquals(
+ "application/json",
+ new
JakartaMediaRangeList("text/html;q=0,*/*").prefer("text/html",
"application/json"));
+ }
+
+ /**
+ * Uses the most specific matching range's quality rather than the quality
of a broader wildcard.
+ */
+ public void testPreferUsesMostSpecificQuality() {
+ assertEquals(
+ "application/json",
+ new
JakartaMediaRangeList("text/html;q=0.1,*/*;q=0.9").prefer("text/html",
"application/json"));
+ assertEquals(
+ "text/html",
+ new
JakartaMediaRangeList("text/html;q=0.2,text/*;q=0.1,application/json;q=0.15")
+ .prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Requires media parameters to match and gives parameter-specific ranges
precedence.
+ */
+ public void testPreferMatchesMediaParameters() {
+ JakartaMediaRangeList ranges = new
JakartaMediaRangeList("text/html;level=1;q=0,text/html;q=0.5,*/*");
+ assertNull(ranges.prefer("text/html;level=1"));
+ assertEquals("text/html;level=2", ranges.prefer("text/html;level=2"));
+ assertEquals("text/html", ranges.prefer("text/html"));
+ assertNull(new
JakartaMediaRangeList("text/html;level=1").prefer("text/html;level=2"));
+ }
+
+ public void testParameterLookupIsCaseInsensitive() {
+ JakartaMediaRangeList.MediaRange range = rangeList.new
MediaRange("text/html;Charset=UTF-8");
+ for (String name : new String[] {"Charset", "charset", "CHARSET"}) {
+ assertEquals(name, "UTF-8", range.getParameter(name));
+ }
+ assertNull(range.getParameter(null));
+ }
+
+ /**
+ * Ignores accept extensions after q when matching ranges and determining
specificity.
+ */
+ public void testPreferIgnoresAcceptExtensions() {
+ assertEquals("text/html", new
JakartaMediaRangeList("text/html;q=1;extension=value").prefer("text/html"));
+ assertNull(new
JakartaMediaRangeList("text/html;q=0.5;extension=value,text/html;level=1;q=0")
+ .prefer("text/html;level=1", "application/json"));
+ }
+
+ /**
+ * Accepts whitespace around quality parameters and rejects invalid or
non-finite quality values.
+ */
+ public void testPreferHandlesQualityWhitespaceAndInvalidValues() {
+ for (String accept :
+ new String[] {"text/html; q=0", "text/html;q=NaN",
"text/html;q=Infinity", "text/html;q=invalid"}) {
+ assertNull(accept, new
JakartaMediaRangeList(accept).prefer("text/html"));
+ }
+ assertEquals(
+ "application/json",
+ new JakartaMediaRangeList("Text/HTML; q=0.2, application/json;
q=0.8")
+ .prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Preserves explicit-type tie-breaking and returns the first offered
instance for a shared wildcard match.
+ */
+ public void testPreferPreservesExplicitTieAndOfferedInstance() {
+ assertEquals(
+ "application/json",
+ new
JakartaMediaRangeList("text/html,application/json").prefer("text/html",
"application/json"));
+ JakartaMediaRangeList ranges = new JakartaMediaRangeList("text/*");
+ JakartaMediaRangeList.MediaRange html = ranges.new
MediaRange("text/html");
+ Set<JakartaMediaRangeList.MediaRange> offered = new LinkedHashSet<>();
+ offered.add(html);
+ offered.add(ranges.new MediaRange("text/plain"));
+ assertSame(html, ranges.prefer(offered));
+ }
+
public void testHttpEquivParam() {
SlingJakartaHttpServletRequest req =
Mockito.mock(SlingJakartaHttpServletRequest.class);
Mockito.when(req.getHeader(JakartaMediaRangeList.HEADER_ACCEPT)).thenReturn("text/plain");
diff --git
a/src/test/java/org/apache/sling/api/request/header/MediaRangeListTest.java
b/src/test/java/org/apache/sling/api/request/header/MediaRangeListTest.java
index 43b67ec..36bf18f 100644
--- a/src/test/java/org/apache/sling/api/request/header/MediaRangeListTest.java
+++ b/src/test/java/org/apache/sling/api/request/header/MediaRangeListTest.java
@@ -18,6 +18,9 @@
*/
package org.apache.sling.api.request.header;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
import junit.framework.TestCase;
import org.apache.sling.api.SlingHttpServletRequest;
import org.mockito.Mockito;
@@ -47,6 +50,103 @@ public class MediaRangeListTest extends TestCase {
assertEquals("application/json", rangeList.prefer("text/html",
"application/json"));
}
+ /**
+ * Matches wildcard ranges and treats missing or empty Accept headers as
accepting all types.
+ */
+ public void testPreferWildcardsAndMissingHeader() {
+ for (String accept : new String[] {null, "", "*/*", "text/*"}) {
+ assertEquals(accept, "text/html", new
MediaRangeList(accept).prefer("text/html", "application/json"));
+ }
+ assertEquals("application/json", new
MediaRangeList("application/*").prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Rejects unsupported types and q=0 ranges, including exclusions that
override broader wildcards.
+ */
+ public void testPreferExcludesUnacceptableTypes() {
+ for (String accept : new String[] {
+ "text/plain",
+ "*/*;q=0",
+ "text/html;q=0",
+ "text/html;q=0,application/json;q=0,*/*",
+ "text/*;q=0,application/*;q=0,*/*"
+ }) {
+ assertNull(accept, new MediaRangeList(accept).prefer("text/html",
"application/json"));
+ }
+ assertEquals(
+ "application/json", new
MediaRangeList("text/html;q=0,*/*").prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Uses the most specific matching range's quality rather than the quality
of a broader wildcard.
+ */
+ public void testPreferUsesMostSpecificQuality() {
+ assertEquals(
+ "application/json",
+ new
MediaRangeList("text/html;q=0.1,*/*;q=0.9").prefer("text/html",
"application/json"));
+ assertEquals(
+ "text/html",
+ new
MediaRangeList("text/html;q=0.2,text/*;q=0.1,application/json;q=0.15")
+ .prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Requires media parameters to match and gives parameter-specific ranges
precedence.
+ */
+ public void testPreferMatchesMediaParameters() {
+ MediaRangeList ranges = new
MediaRangeList("text/html;level=1;q=0,text/html;q=0.5,*/*");
+ assertNull(ranges.prefer("text/html;level=1"));
+ assertEquals("text/html;level=2", ranges.prefer("text/html;level=2"));
+ assertEquals("text/html", ranges.prefer("text/html"));
+ assertNull(new
MediaRangeList("text/html;level=1").prefer("text/html;level=2"));
+ }
+
+ public void testParameterLookupIsCaseInsensitive() {
+ MediaRangeList.MediaRange range = rangeList.new
MediaRange("text/html;Charset=UTF-8");
+ for (String name : new String[] {"Charset", "charset", "CHARSET"}) {
+ assertEquals(name, "UTF-8", range.getParameter(name));
+ }
+ assertNull(range.getParameter(null));
+ }
+
+ /**
+ * Ignores accept extensions after q when matching ranges and determining
specificity.
+ */
+ public void testPreferIgnoresAcceptExtensions() {
+ assertEquals("text/html", new
MediaRangeList("text/html;q=1;extension=value").prefer("text/html"));
+ assertNull(new
MediaRangeList("text/html;q=0.5;extension=value,text/html;level=1;q=0")
+ .prefer("text/html;level=1", "application/json"));
+ }
+
+ /**
+ * Accepts whitespace around quality parameters and rejects invalid or
non-finite quality values.
+ */
+ public void testPreferHandlesQualityWhitespaceAndInvalidValues() {
+ for (String accept :
+ new String[] {"text/html; q=0", "text/html;q=NaN",
"text/html;q=Infinity", "text/html;q=invalid"}) {
+ assertNull(accept, new MediaRangeList(accept).prefer("text/html"));
+ }
+ assertEquals(
+ "application/json",
+ new MediaRangeList("Text/HTML; q=0.2, application/json; q=0.8")
+ .prefer("text/html", "application/json"));
+ }
+
+ /**
+ * Preserves explicit-type tie-breaking and returns the first offered
instance for a shared wildcard match.
+ */
+ public void testPreferPreservesExplicitTieAndOfferedInstance() {
+ assertEquals(
+ "application/json",
+ new
MediaRangeList("text/html,application/json").prefer("text/html",
"application/json"));
+ MediaRangeList ranges = new MediaRangeList("text/*");
+ MediaRangeList.MediaRange html = ranges.new MediaRange("text/html");
+ Set<MediaRangeList.MediaRange> offered = new LinkedHashSet<>();
+ offered.add(html);
+ offered.add(ranges.new MediaRange("text/plain"));
+ assertSame(html, ranges.prefer(offered));
+ }
+
public void testHttpEquivParam() {
SlingHttpServletRequest req =
Mockito.mock(SlingHttpServletRequest.class);
Mockito.when(req.getHeader(MediaRangeList.HEADER_ACCEPT)).thenReturn("text/plain");