Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/finegrain/VariantTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/finegrain/VariantTest.java (revision 774) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/finegrain/VariantTest.java (working copy) @@ -1,353 +0,0 @@ -package org.jboss.resteasy.test.finegrain; - -import org.jboss.resteasy.util.AcceptableVariant; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Variant; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class VariantTest -{ - @BeforeClass - public static void start() - { - } - - @Test - public void testAdd() - { - MediaType applicationXml = new MediaType("application", "xml"); - MediaType textPlain = new MediaType("text", "plain"); - MediaType textHtml = new MediaType("text", "html"); - - Variant.VariantListBuilder builder = Variant.VariantListBuilder.newInstance(); - - builder.languages(new Locale("en"), new Locale("fr")); - - builder.add(); - List variants = builder.build(); - - Assert.assertEquals(2, variants.size()); - - printVariants(variants); - - System.out.println("--------"); - - builder.languages(new Locale("en")).encodings("gzip", "octet").mediaTypes(applicationXml); - variants = builder.build(); - - Assert.assertEquals(2, variants.size()); - - printVariants(variants); - - System.out.println("--------"); - - builder.languages(new Locale("en"), new Locale("es")).mediaTypes(applicationXml, textPlain, textHtml); - variants = builder.build(); - - Assert.assertEquals(6, variants.size()); - printVariants(variants); - - System.out.println("--------"); - - builder.languages(new Locale("en"), new Locale("es")).mediaTypes(applicationXml, textPlain, textHtml).encodings("zip"); - variants = builder.build(); - - Assert.assertEquals(6, variants.size()); - printVariants(variants); - } - - @Test - public void testVariantSorting() - { - List acceptable = new ArrayList(); - - - AcceptableVariant variant1 = new AcceptableVariant(MediaType.valueOf("text/plain"), "en;q=0.3", null); - AcceptableVariant variant2 = new AcceptableVariant(MediaType.valueOf("text/plain"), "fr", null); - AcceptableVariant variant3 = new AcceptableVariant(MediaType.valueOf("text/plain"), "zh;q=0.6", null); - - acceptable.add(variant1); - acceptable.add(variant2); - acceptable.add(variant3); - - List variants = AcceptableVariant.sort(acceptable); - VariantTest.printVariants(variants); - - Assert.assertTrue(acceptable.get(0) == variant2); - Assert.assertTrue(acceptable.get(1) == variant3); - Assert.assertTrue(acceptable.get(2) == variant1); - - - System.out.println("--------"); - } - - @Test - public void testVariantSorting2() - { - List acceptable = new ArrayList(); - - - AcceptableVariant variant1 = new AcceptableVariant(MediaType.valueOf("text/plain"), "en;q=0.3", null); - AcceptableVariant variant2 = new AcceptableVariant(MediaType.valueOf("text/html;q=0.4"), "fr", null); - AcceptableVariant variant3 = new AcceptableVariant(MediaType.valueOf("text/html"), "es", null); - AcceptableVariant variant4 = new AcceptableVariant(MediaType.valueOf("text/plain"), "zh;q=0.6", null); - - acceptable.add(variant1); - acceptable.add(variant2); - acceptable.add(variant3); - acceptable.add(variant4); - - List variants = AcceptableVariant.sort(acceptable); - VariantTest.printVariants(variants); - - Assert.assertTrue(acceptable.get(0) == variant3); - Assert.assertTrue(acceptable.get(1) == variant4); - Assert.assertTrue(acceptable.get(2) == variant1); - Assert.assertTrue(acceptable.get(3) == variant2); - - System.out.println("--------"); - } - - @Test - public void testVariantSorting3() - { - List acceptable = new ArrayList(); - - - AcceptableVariant variant1 = new AcceptableVariant(MediaType.valueOf("text/plain"), "en;q=0.3", null); - AcceptableVariant variant2 = new AcceptableVariant(MediaType.valueOf("text/html;q=0.4"), "fr", null); - AcceptableVariant variant3 = new AcceptableVariant(MediaType.valueOf("text/html"), "es", null); - AcceptableVariant variant4 = new AcceptableVariant(null, "zh;q=0.6", null); - AcceptableVariant variant5 = new AcceptableVariant(MediaType.valueOf("application/xml"), "es", "gzip"); - - - acceptable.add(variant1); - acceptable.add(variant2); - acceptable.add(variant3); - acceptable.add(variant4); - acceptable.add(variant5); - - List variants = AcceptableVariant.sort(acceptable); - VariantTest.printVariants(variants); - - Assert.assertTrue(acceptable.get(0) == variant5); - Assert.assertTrue(acceptable.get(1) == variant3); - Assert.assertTrue(acceptable.get(2) == variant1); - Assert.assertTrue(acceptable.get(3) == variant2); - Assert.assertTrue(acceptable.get(4) == variant4); - - System.out.println("--------"); - } - - @Test - public void testGetLanguageEn() - { - List has = Variant.VariantListBuilder.newInstance(). - languages(new Locale("zh")). - languages(new Locale("fr")). - languages(new Locale("en")).add(). - build(); - - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(null, "en", null)); - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNotNull(v); - Assert.assertNull(v.getMediaType()); - Assert.assertNull(v.getEncoding()); - Assert.assertEquals(v.getLanguage(), new Locale("en")); - } - - @Test - public void testGetLanguageZh() - { - List has = Variant.VariantListBuilder.newInstance(). - languages(new Locale("zh")). - languages(new Locale("fr")). - languages(new Locale("en")).add(). - build(); - - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(null, "zh", null)); - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNotNull(v); - Assert.assertNull(v.getMediaType()); - Assert.assertNull(v.getEncoding()); - Assert.assertEquals(v.getLanguage(), new Locale("zh")); - } - - @Test - public void testGetLanguageMultiple() - { - List has = Variant.VariantListBuilder.newInstance(). - languages(new Locale("zh")). - languages(new Locale("fr")). - languages(new Locale("en")).add(). - build(); - - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(null, "zh;q=0.4", null)); - acceptable.add(new AcceptableVariant(null, "en;q=0.3", null)); - acceptable.add(new AcceptableVariant(null, "fr", null)); - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNotNull(v); - Assert.assertNull(v.getMediaType()); - Assert.assertNull(v.getEncoding()); - Assert.assertEquals(v.getLanguage(), new Locale("fr")); - - } - - @Test - public void testGetComplex1() - { - List has = Variant.VariantListBuilder.newInstance(). - mediaTypes(MediaType.valueOf("image/jpeg")).add(). - mediaTypes(MediaType.valueOf("application/xml")).languages(new Locale("en", "us")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en", "us")).add(). - build(); - - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xhtml+xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("image/png"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/html;q=0.9"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/plain;q=0.8"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("*/*;q=0.5"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/xml"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xhtml+xml"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("image/png"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/html;q=0.9"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/plain;q=0.8"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("*/*;q=0.5"), "en;q=0.5", null)); - - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNotNull(v); - Assert.assertNotNull(v.getMediaType()); - Assert.assertTrue(MediaType.valueOf("text/xml").equals(v.getMediaType())); - Assert.assertNull(v.getEncoding()); - Assert.assertEquals(v.getLanguage(), new Locale("en", "us")); - - } - - @Test - public void testGetComplex2() - { - List has = Variant.VariantListBuilder.newInstance(). - mediaTypes(MediaType.valueOf("image/jpeg")).add(). - mediaTypes(MediaType.valueOf("application/xml")).languages(new Locale("en", "us")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en", "us")).add(). - build(); - - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/xml"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xhtml+xml"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("image/png"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/html;q=0.9"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/plain;q=0.8"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("*/*;q=0.5"), "en", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xhtml+xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("image/png"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/html;q=0.9"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/plain;q=0.8"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("*/*;q=0.5"), "en-us", null)); - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNotNull(v); - Assert.assertNotNull(v.getMediaType()); - Assert.assertTrue(MediaType.valueOf("text/xml").equals(v.getMediaType())); - Assert.assertNull(v.getEncoding()); - Assert.assertEquals(v.getLanguage(), new Locale("en")); - - } - - @Test - public void testGetComplex3() - { - List has = Variant.VariantListBuilder.newInstance(). - mediaTypes(MediaType.valueOf("image/jpeg")).add(). - mediaTypes(MediaType.valueOf("application/xml")).languages(new Locale("en", "us")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en", "us")).add(). - build(); - - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xhtml+xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("image/png"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/html;q=0.9"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/plain;q=0.8"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("*/*;q=0.5"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/xml"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xhtml+xml"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("image/png"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/html;q=0.9"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("text/plain;q=0.8"), "en;q=0.5", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("*/*;q=0.5"), "en;q=0.5", null)); - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNotNull(v); - Assert.assertNotNull(v.getMediaType()); - Assert.assertEquals(MediaType.valueOf("application/xml"), v.getMediaType()); - Assert.assertNull(v.getEncoding()); - Assert.assertEquals(v.getLanguage(), new Locale("en", "us")); - - } - - @Test - public void testGetComplexNotAcceptable() - { - List has = Variant.VariantListBuilder.newInstance(). - mediaTypes(MediaType.valueOf("image/jpeg")).add(). - mediaTypes(MediaType.valueOf("application/xml")).languages(new Locale("en", "us")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en")).add(). - mediaTypes(MediaType.valueOf("text/xml")).languages(new Locale("en", "us")).add(). - build(); - { - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/atom+xml"), "en-us", null)); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/atom+xml"), "en", null)); - - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNull(v); - } - - { - List acceptable = new ArrayList(); - acceptable.add(new AcceptableVariant(MediaType.valueOf("application/xml"), "fr", null)); - Variant v = AcceptableVariant.pick(has, acceptable); - Assert.assertNull(v); - } - - - } - - public static void printVariants(List variants) - { - for (Variant variant : variants) - { - System.out.println("Variant: type=" + variant.getMediaType() + " language=" + variant.getLanguage() + " encoding=" + variant.getEncoding()); - } - } -} \ No newline at end of file Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/LocaleQualityValueTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/LocaleQualityValueTest.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/LocaleQualityValueTest.java (revision 0) @@ -0,0 +1,79 @@ +package org.jboss.resteasy.test.core.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import org.junit.Test; + +import org.jboss.resteasy.core.request.AcceptHeaders; +import org.jboss.resteasy.core.request.QualityValue; + + +/** + @author Pascal S. de Kloe + */ +public class LocaleQualityValueTest { + + @Test + public void simple() { + String header = "da, en-gb;q=0.8, en;q=0.7"; + Locale[] locales = { + new Locale("da"), + Locale.UK, + Locale.ENGLISH + }; + QualityValue[] fields = { + QualityValue.DEFAULT, + QualityValue.valueOf("0.8"), + QualityValue.valueOf("0.7"), + }; + assertList(header, locales, fields); + } + + + @Test + public void wildcard() { + String header = "zh, *"; + Locale[] fields = {Locale.CHINESE, null}; + QualityValue[] qualities = { + QualityValue.DEFAULT, + QualityValue.DEFAULT + }; + assertList(header, fields, qualities); + } + + + @Test + public void undefined() { + String header = "en, en-US, en-cockney, i-cherokee, x-pig-latin"; + Locale[] fields = {Locale.ENGLISH, Locale.US}; + QualityValue[] qualities = { + QualityValue.DEFAULT, + QualityValue.DEFAULT + }; + assertList(header, fields, qualities); + } + + + @Test + public void empty() { + assertNull(AcceptHeaders.getLocaleQualityValues(null)); + assertNull(AcceptHeaders.getLocaleQualityValues("")); + assertNull(AcceptHeaders.getLocaleQualityValues(" ")); + } + + + private static void assertList(String header, Locale[] fields, QualityValue[] qualities) { + Map map = AcceptHeaders.getLocaleQualityValues(header); + List expectedKeys = Arrays.asList(fields); + List expectedValues = Arrays.asList(qualities); + assertEquals(expectedKeys, new ArrayList(map.keySet())); + assertEquals(expectedValues, new ArrayList(map.values())); + } + +} Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/QualityValueTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/QualityValueTest.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/QualityValueTest.java (revision 0) @@ -0,0 +1,107 @@ +package org.jboss.resteasy.test.core.request; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +import org.jboss.resteasy.core.request.QualityValue; +import org.jboss.resteasy.spi.BadRequestException; + + +/** + @author Pascal S. de Kloe + */ +public class QualityValueTest { + + @Test + public void zero() { + assertEquals(0, QualityValue.valueOf("0").intValue()); + assertEquals(0, QualityValue.valueOf("0.").intValue()); + assertEquals(0, QualityValue.valueOf("0.0").intValue()); + assertEquals(0, QualityValue.valueOf("0.00").intValue()); + assertEquals(0, QualityValue.valueOf("0.000").intValue()); + } + + + @Test + public void one() { + assertEquals(1000, QualityValue.valueOf("1").intValue()); + assertEquals(1000, QualityValue.valueOf("1.").intValue()); + assertEquals(1000, QualityValue.valueOf("1.0").intValue()); + assertEquals(1000, QualityValue.valueOf("1.00").intValue()); + assertEquals(1000, QualityValue.valueOf("1.000").intValue()); + } + + + @Test + public void fractions() { + assertEquals(1, QualityValue.valueOf("0.001").intValue()); + assertEquals(12, QualityValue.valueOf("0.012").intValue()); + assertEquals(123, QualityValue.valueOf("0.123").intValue()); + assertEquals(120, QualityValue.valueOf("0.12").intValue()); + assertEquals(100, QualityValue.valueOf("0.1").intValue()); + } + + + @Test + public void equivalent() { + assertTrue(QualityValue.valueOf("0.1").equals(QualityValue.valueOf("0.10"))); + assertFalse(QualityValue.valueOf("1.").equals(QualityValue.valueOf("0.999"))); + } + + + @Test + public void comparison() { + assertTrue(QualityValue.LOWEST.compareTo(QualityValue.HIGHEST) < 0); + assertTrue(QualityValue.DEFAULT.compareTo(QualityValue.HIGHEST) == 0); + assertTrue(QualityValue.LOWEST.compareTo(QualityValue.NOT_ACCEPTABLE) > 0); + } + + + @Test(expected=BadRequestException.class) + public void tooLarge() { + QualityValue.valueOf("1.001"); + } + + + @Test(expected=BadRequestException.class) + public void tooSmall() { + QualityValue.valueOf("-0.001"); + } + + + @Test(expected=BadRequestException.class) + public void tooLong() { + QualityValue.valueOf("0.1234"); + } + + + @Test(expected=BadRequestException.class) + public void tooShort() { + QualityValue.valueOf(""); + } + + + @Test(expected=BadRequestException.class) + public void wrongContent() { + QualityValue.valueOf("0,2F"); + } + + + @Test + public void nullContent() { + assertEquals(QualityValue.DEFAULT, QualityValue.valueOf(null)); + } + + + @Test + public void numbers() { + QualityValue x = QualityValue.valueOf("0.08"); + assertEquals(80, x.intValue()); + assertEquals(80L, x.longValue()); + assertEquals(0.08f, x.floatValue()); + assertEquals(0.08d, x.doubleValue()); + } + +} \ No newline at end of file Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/MediaTypeQualityValueTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/MediaTypeQualityValueTest.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/MediaTypeQualityValueTest.java (revision 0) @@ -0,0 +1,104 @@ +package org.jboss.resteasy.test.core.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import org.junit.Test; + +import org.jboss.resteasy.core.request.AcceptHeaders; +import org.jboss.resteasy.core.request.QualityValue; +import org.jboss.resteasy.spi.BadRequestException; + + +/** + @author Pascal S. de Kloe + */ +public class MediaTypeQualityValueTest { + + @Test + public void simple() { + String header = "audio/*; q=0.2, audio/basic"; + MediaType[] fields = { + MediaType.valueOf("audio/*"), + MediaType.valueOf("audio/basic") + }; + QualityValue[] qualities = { + QualityValue.valueOf("0.2"), + QualityValue.DEFAULT + }; + assertList(header, fields, qualities); + } + + + @Test + public void parameters() { + String header = "text/html;level=\"1\", text/html;level=2;q=0.4"; + MediaType[] fields = { + MediaType.valueOf("text/html;level=1"), + MediaType.valueOf("text/html;level=2") + }; + QualityValue[] qualities = { + QualityValue.DEFAULT, + QualityValue.valueOf("0.4") + }; + assertList(header, fields, qualities); + } + + + @Test + public void unsupportedExtension() { + String header = "plain/text; a=b; q=0.2; extension=unsupported"; + MediaType[] fields = {MediaType.valueOf("plain/text;a=b")}; + QualityValue[] qualities = {QualityValue.NOT_ACCEPTABLE}; + assertList(header, fields, qualities); + } + + + @Test + public void badRequests() { + String[] badHeaders = { + "a", + "a,b", + "a/b,", + "a/b;", + "a/b;p", + "a/b;p=x,", + "a/b;p=\"x\"y", + "a/b;p=\"x\"y,c/d", + "a/b;p=\"x,c/d", + "a/b;p=\"x\\\",c/d" + }; + for (String header : badHeaders) { + try { + AcceptHeaders.getMediaTypeQualityValues(header); + fail(header); + } catch (BadRequestException e) { + } + } + } + + + @Test + public void empty() { + assertNull(AcceptHeaders.getMediaTypeQualityValues(null)); + assertNull(AcceptHeaders.getMediaTypeQualityValues("")); + assertNull(AcceptHeaders.getMediaTypeQualityValues(" ")); + } + + + private static void assertList(String header, MediaType[] fields, QualityValue[] qualities) { + Map map = AcceptHeaders.getMediaTypeQualityValues(header); + List expectedKeys = Arrays.asList(fields); + List expectedValues = Arrays.asList(qualities); + assertEquals(expectedKeys, new ArrayList(map.keySet())); + assertEquals(expectedValues, new ArrayList(map.values())); + } + +} Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/VariantSelectionTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/VariantSelectionTest.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/VariantSelectionTest.java (revision 0) @@ -0,0 +1,114 @@ +package org.jboss.resteasy.test.core.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Variant; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; + +import org.jboss.resteasy.core.request.ServerDrivenNegotiation; + + +/** + @author Pascal S. de Kloe + */ +public class VariantSelectionTest { + + @Test + public void mostSpecific() { + ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation(); + negotiation.setAcceptHeaders(Arrays.asList("text/plain")); + negotiation.setAcceptCharsetHeaders(Arrays.asList("UTF-8")); + negotiation.setAcceptEncodingHeaders(Arrays.asList("gzip")); + negotiation.setAcceptLanguageHeaders(Arrays.asList("en-gb")); + + MediaType mediaTypeWithCharset = MediaType.valueOf("text/plain; charset=UTF-8"); + MediaType mediaType = MediaType.valueOf("text/plain"); + String encoding = "gzip"; + Locale locale = Locale.UK; + + List available = new ArrayList(); + available.add(new Variant(mediaTypeWithCharset, null, null)); + available.add(new Variant(mediaTypeWithCharset, locale, null)); + available.add(new Variant(mediaTypeWithCharset, null, encoding)); + available.add(new Variant(mediaTypeWithCharset, locale, encoding)); + available.add(new Variant(mediaType, null, null)); + available.add(new Variant(mediaType, locale, null)); + available.add(new Variant(mediaType, null, encoding)); + available.add(new Variant(mediaType, locale, encoding)); + available.add(new Variant(null, locale, null)); + available.add(new Variant(null, locale, encoding)); + available.add(new Variant(null, null, encoding)); + + // Assert all acceptable: + for (Variant variant : available) + assertEquals(variant, negotiation.getBestMatch(Arrays.asList(variant))); + + Variant best = negotiation.getBestMatch(available); + assertNotNull(best); + assertEquals(mediaTypeWithCharset, best.getMediaType()); + assertEquals(encoding, best.getEncoding()); + assertEquals(locale, best.getLanguage()); + } + + + @Test + public void mostSpecificMediaType() { + String header ="text/*, text/html, text/html;level=1, */*"; + ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation(); + negotiation.setAcceptHeaders(Arrays.asList(header)); + + Variant o1 = new Variant(MediaType.valueOf("text/html;level=1"), null, null); + Variant o2 = new Variant(MediaType.valueOf("text/html"), null, null); + Variant o3 = new Variant(MediaType.valueOf("text/*"), null, null); + Variant o4 = new Variant(MediaType.valueOf("*/*"), null, null); + + List available = new ArrayList(); + available.add(o4); + assertEquals(o4, negotiation.getBestMatch(available)); + available.add(o3); + assertEquals(o3, negotiation.getBestMatch(available)); + available.add(o2); + assertEquals(o2, negotiation.getBestMatch(available)); + available.add(o1); + assertEquals(o1, negotiation.getBestMatch(available)); + } + + + @Test + public void mediaTypeQualityFactor() { + String header1 = "text/*;q=0.3, text/html;q=0.7, text/html;level=1"; + String header2 = "text/html;level=2;q=0.4, */*;q=0.5"; + ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation(); + negotiation.setAcceptHeaders(Arrays.asList(header1, header2)); + negotiation.setAcceptLanguageHeaders(Arrays.asList("en")); + + Variant q03 = new Variant(MediaType.valueOf("text/plain"), null, null); + Variant q04 = new Variant(MediaType.valueOf("text/html;level=2"), null, null); + Variant q05 = new Variant(MediaType.valueOf("image/jpeg"), null, null); + Variant q07 = new Variant(MediaType.valueOf("text/html"), null, null); + Variant q07plus = new Variant(MediaType.valueOf("text/html;level=3"), null, null); + Variant q10 = new Variant(MediaType.valueOf("text/html;level=1"), null, null); + + List available = new ArrayList(); + available.add(q03); + assertEquals(q03, negotiation.getBestMatch(available)); + available.add(q04); + assertEquals(q04, negotiation.getBestMatch(available)); + available.add(q05); + assertEquals(q05, negotiation.getBestMatch(available)); + available.add(q07); + assertEquals(q07, negotiation.getBestMatch(available)); + available.add(q07plus); + assertEquals(q07plus, negotiation.getBestMatch(available)); + available.add(q10); + assertEquals(q10, negotiation.getBestMatch(available)); + } + +} Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/StringQualityValueTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/StringQualityValueTest.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/StringQualityValueTest.java (revision 0) @@ -0,0 +1,109 @@ +package org.jboss.resteasy.test.core.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import org.junit.Test; + +import org.jboss.resteasy.core.request.AcceptHeaders; +import org.jboss.resteasy.core.request.QualityValue; +import org.jboss.resteasy.spi.BadRequestException; + + +/** + @author Pascal S. de Kloe + */ +public class StringQualityValueTest { + + @Test + public void simple() { + String[] fields = {"compress", "gzip"}; + QualityValue[] qualities = { + QualityValue.DEFAULT, + QualityValue.DEFAULT + }; + assertList("compress, gzip", fields, qualities); + assertList(" compress,gzip ", fields, qualities); + assertList("compress ,gzip", fields, qualities); + } + + + @Test + public void parameter() { + String header = "iso-8859-5, unicode-1-1;q=0.8"; + String[] fields = {"iso-8859-5", "unicode-1-1"}; + QualityValue[] qualities = { + QualityValue.DEFAULT, + QualityValue.valueOf("0.8") + }; + assertList(header, fields, qualities); + } + + + @Test + public void wildcard() { + String header = "*"; + String[] fields = {null}; + QualityValue[] qualities = {QualityValue.DEFAULT}; + assertList(header, fields, qualities); + } + + + @Test + public void wildcardWithParameter() { + String header = "gzip;q=1.0, identity; q=0.5, *;q=0"; + String[] fields = {"gzip", "identity", null}; + QualityValue[] qualities = { + QualityValue.valueOf("1.0"), + QualityValue.valueOf("0.5"), + QualityValue.valueOf("0") + }; + assertList(header, fields, qualities); + } + + + @Test + public void badRequests() { + String[] badHeaders = { + " ,b,c", // empty fields + "a, ,c", + "a,b, ", + ",", + "a;", // empty parameters + "a;,b", + "a;x=0", // illegal parameters + "a;q=0.1;q=0.1", + "a;illegal" + }; + for (String header : badHeaders) { + try { + AcceptHeaders.getStringQualityValues(header); + fail(header); + } catch (BadRequestException e) { + } + } + } + + + @Test + public void empty() { + assertNull(AcceptHeaders.getStringQualityValues(null)); + assertNull(AcceptHeaders.getStringQualityValues("")); + assertNull(AcceptHeaders.getStringQualityValues(" ")); + } + + + private static void assertList(String header, String[] fields, QualityValue[] qualities) { + Map map = AcceptHeaders.getStringQualityValues(header); + List expectedKeys = Arrays.asList(fields); + List expectedValues = Arrays.asList(qualities); + assertEquals(expectedKeys, new ArrayList(map.keySet())); + assertEquals(expectedValues, new ArrayList(map.values())); + } + +} Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/VariantQualityTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/VariantQualityTest.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/core/request/VariantQualityTest.java (revision 0) @@ -0,0 +1,51 @@ +package org.jboss.resteasy.test.core.request; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +import org.jboss.resteasy.core.request.QualityValue; +import org.jboss.resteasy.core.request.VariantQuality; + + +/** + @author Pascal S. de Kloe + */ +public class VariantQualityTest { + + @Test + public void defaultQuality() { + VariantQuality q = new VariantQuality(); + assertEquals(new BigDecimal("1.00000"), q.getOverallQuality()); + q.setMediaTypeQualityValue(null); + q.setCharacterSetQualityValue(null); + q.setEncodingQualityValue(null); + q.setLanguageQualityValue(null); + assertEquals(new BigDecimal("1.00000"), q.getOverallQuality()); + } + + + @Test + public void qualitySetters() { + VariantQuality q = new VariantQuality(); + q.setMediaTypeQualityValue(QualityValue.valueOf("0.1")); + assertEquals(new BigDecimal("0.10000"), q.getOverallQuality()); + q.setCharacterSetQualityValue(QualityValue.valueOf("0.2")); + assertEquals(new BigDecimal("0.02000"), q.getOverallQuality()); + q.setEncodingQualityValue(QualityValue.valueOf("0.4")); + assertEquals(new BigDecimal("0.00800"), q.getOverallQuality()); + q.setLanguageQualityValue(QualityValue.valueOf("0.8")); + assertEquals(new BigDecimal("0.00640"), q.getOverallQuality()); + } + + + @Test + public void round5() { + VariantQuality q = new VariantQuality(); + q.setMediaTypeQualityValue(QualityValue.valueOf("0.004")); + q.setEncodingQualityValue(QualityValue.valueOf("0.008")); + assertEquals(new BigDecimal("0.00003"), q.getOverallQuality()); + } + +} Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/ServerDrivenNegotiation.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/ServerDrivenNegotiation.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/ServerDrivenNegotiation.java (revision 0) @@ -0,0 +1,304 @@ +package org.jboss.resteasy.core.request; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Variant; + + +/** + * {@link Variant} selection. + @see "RFC 2296" + @author Pascal S. de Kloe + */ +public class ServerDrivenNegotiation { + + private Map requestedMediaTypes = null; + private Map requestedCharacterSets = null; + private Map requestedEncodings = null; + private Map requestedLanguages = null; + + + public ServerDrivenNegotiation() { + } + + + public void setAcceptHeaders(List headerValues) { + requestedMediaTypes = null; + if (headerValues == null) + return; + Map requested = null; + for (String headerValue : headerValues) { + Map mapping = AcceptHeaders.getMediaTypeQualityValues(headerValue); + if (mapping == null) + return; + if (requested == null) + requested = mapping; + else + requested.putAll(mapping); + } + requestedMediaTypes = requested; + } + + + public void setAcceptCharsetHeaders(List headerValues) { + requestedCharacterSets = null; + if (headerValues == null) + return; + Map requested = null; + for (String headerValue : headerValues) { + Map mapping = AcceptHeaders.getStringQualityValues(headerValue); + if (mapping == null) + return; + if (requested == null) + requested = mapping; + else + requested.putAll(mapping); + } + requestedCharacterSets = requested; + } + + + public void setAcceptEncodingHeaders(List headerValues) { + requestedEncodings = null; + if (headerValues == null) + return; + Map requested = null; + for (String headerValue : headerValues) { + Map mapping = AcceptHeaders.getStringQualityValues(headerValue); + if (mapping == null) + return; + if (requested == null) + requested = mapping; + else + requested.putAll(mapping); + } + requestedEncodings = requested; + } + + + public void setAcceptLanguageHeaders(List headerValues) { + requestedLanguages = null; + if (headerValues == null) + return; + Map requested = null; + for (String headerValue : headerValues) { + Map mapping = AcceptHeaders.getLocaleQualityValues(headerValue); + if (mapping == null) + return; + if (requested == null) + requested = mapping; + else + requested.putAll(mapping); + } + requestedLanguages = requested; + } + + + public Variant getBestMatch(List available) { + BigDecimal bestQuality = BigDecimal.ZERO; + Variant bestOption = null; + for (Variant option : available) { + VariantQuality quality = new VariantQuality(); + if (! applyMediaType(option, quality)) + continue; + if (! applyCharacterSet(option, quality)) + continue; + if (! applyEncoding(option, quality)) + continue; + if (! applyLanguage(option, quality)) + continue; + + BigDecimal optionQuality = quality.getOverallQuality(); + if (isBetterOption(bestQuality, bestOption, optionQuality, option)) { + bestQuality = optionQuality; + bestOption = option; + } + } + return bestOption; + } + + + /** + * Tests whether {@code option} is preferable over the current {@code bestOption}. + */ + private static boolean isBetterOption(BigDecimal bestQuality, Variant best, + BigDecimal optionQuality, Variant option) { + if (best == null) + return true; + MediaType bestType = best.getMediaType(); + MediaType optionType = option.getMediaType(); + if (bestType != null && optionType != null) { + if (bestType.getType().equals(optionType.getType())) { + // Same type + if (bestType.getSubtype().equals(optionType.getSubtype())) { + // Same subtype + int bestCount = bestType.getParameters().size(); + int optionCount = optionType.getParameters().size(); + if (optionCount > bestCount) + return true; // more matching parameters + else if (optionCount < bestCount) + return false; // less matching parameters + } else if ("*".equals(bestType.getSubtype())) { + return true; // more specific subtype + } else if ("*".equals(optionType.getSubtype())) { + return false; // less specific subtype + } + } else if ("*".equals(bestType.getType())) { + return true; // more specific type + } else if ("*".equals(optionType.getType())) { + return false; // less specific type; + } + } + + int signum = bestQuality.compareTo(optionQuality); + if (signum != 0) + return signum < 0; + return getExplicitness(best) < getExplicitness(option); + } + + + private static int getExplicitness(Variant variant) { + int explicitness = 0; + if (variant.getMediaType() != null) + ++explicitness; + if (variant.getEncoding() != null) + ++explicitness; + if (variant.getLanguage() != null) + ++explicitness; + return explicitness; + } + + + private boolean applyMediaType(Variant option, VariantQuality quality) { + if (requestedMediaTypes == null) + return true; + MediaType mediaType = option.getMediaType(); + if (mediaType == null) + return true; + + String type = mediaType.getType(); + if ("*".equals(type)) + type = null; + String subtype = mediaType.getSubtype(); + if ("*".equals(subtype)) + subtype = null; + Map parameters = mediaType.getParameters(); + if (parameters.isEmpty()) + parameters = null; + + QualityValue bestQuality = QualityValue.NOT_ACCEPTABLE; + int bestMatchCount = -1; + + for (MediaType requested : requestedMediaTypes.keySet()) { + int matchCount = 0; + if (type != null) { + String requestedType = requested.getType(); + if (requestedType.equals(type)) + ++matchCount; + else if (! "*".equals(requestedType)) + continue; + } + if (subtype != null) { + String requestedSubtype = requested.getSubtype(); + if (requestedSubtype.equals(subtype)) + ++matchCount; + else if (! "*".equals(requestedSubtype)) + continue; + } + if (parameters != null) { + Map requestedParameters = requested.getParameters(); + if (! hasRequiredParameters(requestedParameters, parameters)) + continue; + matchCount += requestedParameters.size(); + } + + if (matchCount > bestMatchCount) { + bestMatchCount = matchCount; + bestQuality = requestedMediaTypes.get(requested); + } else if (matchCount == bestMatchCount) { + QualityValue qualityValue = requestedMediaTypes.get(requested); + if (bestQuality.compareTo(qualityValue) < 0) + bestQuality = qualityValue; + } + } + + if (! bestQuality.isAcceptable()) + return false; + quality.setMediaTypeQualityValue(bestQuality); + return true; + } + + + private boolean hasRequiredParameters(Map required, Map available) { + for (Entry requiredEntry : required.entrySet()) { + String name = requiredEntry.getKey(); + String value = requiredEntry.getValue(); + String availableValue = available.get(name); + if (availableValue == null && "charset".equals(name)) { + if (requestedCharacterSets != null + && ! requestedCharacterSets.containsKey(null) + && ! requestedCharacterSets.containsKey(value)) + return false; + } else if (! value.equals(availableValue)) + return false; + } + return true; + } + + + private boolean applyCharacterSet(Variant option, VariantQuality quality) { + if (requestedCharacterSets == null) + return true; + MediaType mediaType = option.getMediaType(); + if (mediaType == null) + return true; + String charsetParameter = mediaType.getParameters().get("charset"); + if (charsetParameter == null) + return true; + QualityValue value = requestedCharacterSets.get(charsetParameter); + if (value == null) // try wildcard + value = requestedCharacterSets.get(null); + if (value == null) // no match + return false; + quality.setCharacterSetQualityValue(value); + return true; + } + + + private boolean applyEncoding(Variant option, VariantQuality quality) { + if (requestedEncodings == null) + return true; + String encoding = option.getEncoding(); + if (encoding == null) + return true; + QualityValue value = requestedEncodings.get(encoding); + if (value == null) // try wildcard + value = requestedEncodings.get(null); + if (value == null) // no match + return false; + quality.setEncodingQualityValue(value); + return true; + } + + + private boolean applyLanguage(Variant option, VariantQuality quality) { + if (requestedLanguages == null) + return true; + Locale language = option.getLanguage(); + if (language == null) + return true; + QualityValue value = requestedLanguages.get(language); + if (value == null) // try wildcard + value = requestedLanguages.get(null); + if (value == null) // no match + return false; + quality.setLanguageQualityValue(value); + return true; + } + +} \ No newline at end of file Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/QualityValue.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/QualityValue.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/QualityValue.java (revision 0) @@ -0,0 +1,122 @@ +package org.jboss.resteasy.core.request; + +import org.jboss.resteasy.spi.BadRequestException; + + +/** + @see "RFC2616 3.9 Quality Values" + @author Pascal S. de Kloe + */ +public final class QualityValue extends Number implements Comparable { + + public static final QualityValue NOT_ACCEPTABLE = new QualityValue(0); + public static final QualityValue LOWEST = new QualityValue(1); + public static final QualityValue HIGHEST = new QualityValue(1000); + public static final QualityValue DEFAULT = HIGHEST; + + private static final long serialVersionUID = 1L; + private static final String MALFORMED_VALUE_MESSAGE = "Malformed quality value."; + + private final int WEIGHT; + + + private QualityValue(int weight) { + assert weight >= 0; + assert weight <= 1000; + WEIGHT = weight; + } + + + /** + @param qvalue the quality value or {@code null} if undefined. + */ + public static QualityValue valueOf(String qvalue) { + if (qvalue == null) + return DEFAULT; + return new QualityValue(parseAsInteger(qvalue)); + } + + + public boolean isPrefered() { + return WEIGHT == HIGHEST.WEIGHT; + } + + + public boolean isAcceptable() { + return WEIGHT != NOT_ACCEPTABLE.WEIGHT; + } + + + public int compareTo(QualityValue o) { + return WEIGHT - o.WEIGHT; + } + + + @Override + public boolean equals(Object o) { + if (o == null || o.getClass() != QualityValue.class) + return false; + QualityValue other = (QualityValue) o; + return WEIGHT == other.WEIGHT; + } + + + @Override + public int hashCode() { + return WEIGHT; + } + + + @Override + public double doubleValue() { + return (double) WEIGHT / 1000d; + } + + + @Override + public float floatValue() { + return (float) WEIGHT / 1000f; + } + + + @Override + public int intValue() { + return WEIGHT; + } + + + @Override + public long longValue() { + return WEIGHT; + } + + + private static int parseAsInteger(String value) { + int length = value.length(); + if (length == 0 || length > 5) + throw new BadRequestException(MALFORMED_VALUE_MESSAGE); + if (length > 1 && value.charAt(1) != '.') + throw new BadRequestException(MALFORMED_VALUE_MESSAGE); + int firstCharacter = value.codePointAt(0); + if (firstCharacter == '1') { + for (int i = 2; i < length; ++i) + if (value.charAt(i) != '0') + throw new BadRequestException(MALFORMED_VALUE_MESSAGE); + return 1000; + } else if (firstCharacter == '0') { + int weight = 0; + for (int i = 2; i < 5; ++i) { + weight *= 10; + if (i < length) { + int digit = value.codePointAt(i) - '0'; + if (digit < 0 || digit > 9) + throw new BadRequestException(MALFORMED_VALUE_MESSAGE); + weight += digit; + } + } + return weight; + } else + throw new BadRequestException(MALFORMED_VALUE_MESSAGE); + } + +} Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/AcceptHeaders.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/AcceptHeaders.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/AcceptHeaders.java (revision 0) @@ -0,0 +1,259 @@ +package org.jboss.resteasy.core.request; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.core.LoggerCategories; +import org.jboss.resteasy.spi.BadRequestException; +import org.slf4j.Logger; + + +/** + @author Pascal S. de Kloe + */ +public class AcceptHeaders { + + private static final Logger logger = LoggerCategories.getCoreLogger(); + + + /** + * Gets the strings from a comma-separated list. + * All "*" entries are replaced with {@code null} keys. + @param header the header value. + @return the listed items in order of appearance or {@code null} if the header didn't contain any entries. + */ + public static Map getStringQualityValues(String header) { + if (header == null) + return null; + header = header.trim(); + if (header.length() == 0) + return null; + Map result = new LinkedHashMap(); + + int offset = 0; + while (true) { + int endIndex = header.indexOf(',', offset); + String content; + if (endIndex < 0) + content = header.substring(offset); + else + content = header.substring(offset, endIndex); + + QualityValue qualityValue = QualityValue.DEFAULT; + int qualityIndex = content.indexOf(';'); + if (qualityIndex >= 0) { + String parameter = content.substring(qualityIndex + 1); + content = content.substring(0, qualityIndex); + + int equalsIndex = parameter.indexOf('='); + if (equalsIndex < 0) + throw new BadRequestException("Malformed parameter: " + parameter); + String name = parameter.substring(0, equalsIndex).trim(); + if (! "q".equals(name)) + throw new BadRequestException("Unsupported parameter: " + name); + String value = parameter.substring(equalsIndex + 1).trim(); + qualityValue = QualityValue.valueOf(value); + } + + content = content.trim(); + if (content.length() == 0) + throw new BadRequestException("Empty field in: " + header + "."); + if (content.equals("*")) + result.put(null, qualityValue); + else + result.put(content, qualityValue); + + if (endIndex < 0) + break; + offset = endIndex + 1; + }; + + if (logger.isDebugEnabled()) + logger.debug(result.toString()); + return result; + } + + + /** + * Gets the locales from a comma-separated list. + * Any "*" entries are replaced with {@code null} keys. + @param header the header value. + @return the listed items in order of appearance or {@code null} if the header didn't contain any entries. + */ + public static Map getLocaleQualityValues(String header) { + Map stringResult = getStringQualityValues(header); + if (stringResult == null) + return null; + Map result = new LinkedHashMap(stringResult.size() * 2); + + for (Entry entry : stringResult.entrySet()) { + QualityValue quality = entry.getValue(); + Locale locale = null; + String value = entry.getKey(); + if (value != null) { + int length = value.length(); + if (length == 2) { + locale = new Locale(value); + } else if (length == 5 && value.charAt(2) == '-') { + String language = value.substring(0, 2); + String country = value.substring(3, 5); + locale = new Locale(language, country); + } else { + logger.warn("Ignoring unsupported locale: " + value); + continue; + } + } + result.put(locale, quality); + } + + if (logger.isDebugEnabled()) + logger.debug(result.toString()); + return result; + } + + + /** + * Gets the media types from a comma-separated list. + @param header the header value. + @return the listed items in order of appearance or {@code null} if the header didn't contain any entries. + */ + public static Map getMediaTypeQualityValues(String header) { + if (header == null) + return null; + header = header.trim(); + if (header.length() == 0) + return null; + Map result = new LinkedHashMap(); + + int offset = 0; + while (offset >= 0) { + int slashIndex = header.indexOf('/', offset); + if (slashIndex < 0) + throw new BadRequestException("Malformed media type: " + header); + String type = header.substring(offset, slashIndex); + String subtype; + Map parameters = null; + QualityValue qualityValue = QualityValue.DEFAULT; + + offset = slashIndex + 1; + int parameterStartIndex = header.indexOf(';', offset); + int itemEndIndex = header.indexOf(',', offset); + if (parameterStartIndex == itemEndIndex) { + assert itemEndIndex == -1; + subtype = header.substring(offset); + offset = -1; + } else if (itemEndIndex < 0 || (parameterStartIndex >= 0 && parameterStartIndex < itemEndIndex)) { + subtype = header.substring(offset, parameterStartIndex); + offset = parameterStartIndex + 1; + parameters = new LinkedHashMap(); + offset = parseParameters(parameters, header, offset); + qualityValue = evaluateAcceptParameters(parameters); + } else { + subtype = header.substring(offset, itemEndIndex); + offset = itemEndIndex + 1; + } + result.put(new MediaType(type.trim(), subtype.trim(), parameters), qualityValue); + } + + if (logger.isDebugEnabled()) + logger.debug(result.toString()); + return result; + } + + + private static int parseParameters(Map parameters, String header, int offset) { + while (true) { + int equalsIndex = header.indexOf('=', offset); + if (equalsIndex < 0) + throw new BadRequestException("Malformed parameters: " + header); + String name = header.substring(offset, equalsIndex).trim(); + offset = equalsIndex + 1; + if (header.charAt(offset) == '"') { + int end = offset; + ++offset; + do { + end = header.indexOf('"', ++end); + if (end < 0) + throw new BadRequestException("Quoted string is not closed: " + header); + } while (header.charAt(end - 1) == '\\'); + String value = header.substring(offset, end); + parameters.put(name, value); + offset = end + 1; + + int parameterEndIndex = header.indexOf(';', offset); + int itemEndIndex = header.indexOf(',', offset); + if (parameterEndIndex == itemEndIndex) { + assert itemEndIndex == -1; + if (header.substring(offset).trim().length() != 0) + throw new BadRequestException("Tailing garbage: " + header); + return -1; + } else if (parameterEndIndex < 0 || (itemEndIndex >= 0 && itemEndIndex < parameterEndIndex)) { + if (header.substring(offset, itemEndIndex).trim().length() != 0) + throw new BadRequestException("Garbage after quoted string: " + header); + return itemEndIndex + 1; + } else { + if (header.substring(offset, parameterEndIndex).trim().length() != 0) + throw new BadRequestException("Garbage after quoted string: " + header); + offset = parameterEndIndex + 1; + } + } else { + int parameterEndIndex = header.indexOf(';', offset); + int itemEndIndex = header.indexOf(',', offset); + if (parameterEndIndex == itemEndIndex) { + assert itemEndIndex == -1; + String value = header.substring(offset).trim(); + parameters.put(name, value); + return -1; + } else if (parameterEndIndex < 0 || (itemEndIndex >= 0 && itemEndIndex < parameterEndIndex)) { + String value = header.substring(offset, itemEndIndex).trim(); + parameters.put(name, value); + return itemEndIndex + 1; + } else { + String value = header.substring(offset, parameterEndIndex).trim(); + parameters.put(name, value); + offset = parameterEndIndex + 1; + } + } + } + } + + + /** + * Evaluates and removes the accept parameters. + *
+	 * accept-params  = ";" "q" "=" qvalue *( accept-extension )
+	 * accept-extension = ";" token [ "=" ( token | quoted-string ) ]
+	 * 
+ @param parameters all parameters in order of appearance. + @return the qvalue. + @see "accept-params + */ + private static QualityValue evaluateAcceptParameters(Map parameters) { + Iterator i = parameters.keySet().iterator(); + while (i.hasNext()) { + String name = i.next(); + if ("q".equals(name)) { + if (i.hasNext()) { + logger.warn("Accept extensions not supported."); + i.remove(); + do { + i.next(); + i.remove(); + } while (i.hasNext()); + return QualityValue.NOT_ACCEPTABLE; + } else { + String value = parameters.get(name); + i.remove(); + return QualityValue.valueOf(value); + } + } + } + return QualityValue.DEFAULT; + } + +} Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/VariantQuality.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/VariantQuality.java (revision 0) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/request/VariantQuality.java (revision 0) @@ -0,0 +1,83 @@ +package org.jboss.resteasy.core.request; + +import java.math.BigDecimal; +import java.math.MathContext; + + +/** + * A individual variant quality bean for the RVSA. + @see "RFC 2296" + @author Pascal S. de Kloe + */ +public class VariantQuality { + + private QualityValue mediaTypeQualityValue = QualityValue.DEFAULT; + private QualityValue characterSetQualityValue = QualityValue.DEFAULT; + private QualityValue encodingQualityValue = QualityValue.DEFAULT; + private QualityValue languageQualityValue = QualityValue.DEFAULT; + + + public VariantQuality() { + } + + + public void setMediaTypeQualityValue(QualityValue value) { + if (value == null) + mediaTypeQualityValue = QualityValue.DEFAULT; + else + mediaTypeQualityValue = value; + } + + + public void setCharacterSetQualityValue(QualityValue value) { + if (value == null) + characterSetQualityValue = QualityValue.DEFAULT; + else + characterSetQualityValue = value; + } + + + public void setEncodingQualityValue(QualityValue value) { + if (value == null) + encodingQualityValue = QualityValue.DEFAULT; + else + encodingQualityValue = value; + } + + + public void setLanguageQualityValue(QualityValue value) { + if (value == null) + languageQualityValue = QualityValue.DEFAULT; + else + languageQualityValue = value; + } + + + /** + @return the quality value between zero and one with five decimal places after the point. + @see "3.3 Computing overall quality values" + */ + public BigDecimal getOverallQuality() { + BigDecimal qt = BigDecimal.valueOf(mediaTypeQualityValue.intValue(), 3); + BigDecimal qc = BigDecimal.valueOf(characterSetQualityValue.intValue(), 3); + BigDecimal qe = BigDecimal.valueOf(encodingQualityValue.intValue(), 3); + BigDecimal ql = BigDecimal.valueOf(languageQualityValue.intValue(), 3); + assert qt.compareTo(BigDecimal.ZERO) >= 0 && qt.compareTo(BigDecimal.ONE) <= 0; + assert qc.compareTo(BigDecimal.ZERO) >= 0 && qc.compareTo(BigDecimal.ONE) <= 0; + assert qe.compareTo(BigDecimal.ZERO) >= 0 && qe.compareTo(BigDecimal.ONE) <= 0; + assert ql.compareTo(BigDecimal.ZERO) >= 0 && ql.compareTo(BigDecimal.ONE) <= 0; + + BigDecimal result = qt; + result = result.multiply(qc, MathContext.DECIMAL32); + result = result.multiply(qe, MathContext.DECIMAL32); + result = result.multiply(ql, MathContext.DECIMAL32); + assert result.compareTo(BigDecimal.ZERO) >= 0 && result.compareTo(BigDecimal.ONE) <= 0; + + long round5 = result.scaleByPowerOfTen(5).longValue(); + result = BigDecimal.valueOf(round5, 5); + assert result.compareTo(BigDecimal.ZERO) >= 0 && result.compareTo(BigDecimal.ONE) <= 0; + + return result; + } + +} Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/specimpl/RequestImpl.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/specimpl/RequestImpl.java (revision 841) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/specimpl/RequestImpl.java (working copy) @@ -1,15 +1,13 @@ package org.jboss.resteasy.specimpl; -import org.jboss.resteasy.plugins.server.servlet.ServletUtil; +import org.jboss.resteasy.core.request.ServerDrivenNegotiation; import org.jboss.resteasy.spi.HttpRequest; -import org.jboss.resteasy.util.AcceptableVariant; import org.jboss.resteasy.util.DateUtil; import org.jboss.resteasy.util.HttpHeaderNames; import org.jboss.resteasy.util.HttpResponseCodes; import javax.ws.rs.core.EntityTag; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; @@ -50,71 +48,17 @@ { if (variants == null || variants.size() == 0) throw new IllegalArgumentException("Variant list must not be zero"); - List accepts = headers.getAcceptableMediaTypes(); - List languages = ServletUtil.extractLanguages(headers.getRequestHeaders()); - List encodings = convertString(headers.getRequestHeaders().get(HttpHeaderNames.ACCEPT_ENCODING)); + ServerDrivenNegotiation negotiation = new ServerDrivenNegotiation(); + MultivaluedMap requestHeaders = headers.getRequestHeaders(); + negotiation.setAcceptHeaders(requestHeaders.get(HttpHeaderNames.ACCEPT)); + negotiation.setAcceptCharsetHeaders(requestHeaders.get(HttpHeaderNames.ACCEPT_CHARSET)); + negotiation.setAcceptEncodingHeaders(requestHeaders.get(HttpHeaderNames.ACCEPT_ENCODING)); + negotiation.setAcceptLanguageHeaders(requestHeaders.get(HttpHeaderNames.ACCEPT_LANGUAGE)); - varyHeader = ResponseBuilderImpl.createVaryHeader(variants); - return pickVariant(variants, accepts, languages, encodings); + return negotiation.getBestMatch(variants); } - public static Variant pickVariant(List has, List accepts, List languages, List encodings) - { - List wants = new ArrayList(); - - int langSize = languages.size(); - int encodingSize = encodings.size(); - int typeSize = accepts.size(); - - int i = 0; - - if (langSize > 0 || encodingSize > 0 || typeSize > 0) - { - do - { - MediaType type = null; - if (i < typeSize) type = accepts.get(i); - int j = 0; - do - { - String encoding = null; - if (j < encodingSize) encoding = encodings.get(j); - int k = 0; - do - { - String language = null; - if (k < langSize) language = languages.get(k); - wants.add(new AcceptableVariant(type, language, encoding)); - k++; - } while (k < langSize); - j++; - } while (j < encodingSize); - i++; - } while (i < typeSize); - } - - - return AcceptableVariant.pick(has, wants); - - } - - - public List convertString(List tags) - { - ArrayList result = new ArrayList(); - if (tags == null) return result; - for (String tag : tags) - { - String[] split = tag.split(","); - for (String etag : split) - { - result.add(etag.trim()); - } - } - return result; - } - public List convertEtag(List tags) { ArrayList result = new ArrayList(); Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/QualifiedStringHeader.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/QualifiedStringHeader.java (revision 774) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/QualifiedStringHeader.java (working copy) @@ -1,114 +0,0 @@ -package org.jboss.resteasy.util; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class QualifiedStringHeader -{ - private String value; - private Map parameters = new HashMap(); - private double q = 1.0; - - protected QualifiedStringHeader() - { - - } - - public QualifiedStringHeader(String value) - { - this.value = value; - } - - public String getValue() - { - return value; - } - - public Map getParameters() - { - return parameters; - } - - public double getQ() - { - return q; - } - - public static QualifiedStringHeader parse(String value) - { - QualifiedStringHeader header = new QualifiedStringHeader(); - int idx = value.indexOf(";"); - if (idx < 0) - { - header.value = value; - header.q = 1.0; - } - else - { - header.value = value.substring(0, idx); - String params = value.substring(idx + 1); - if (params.startsWith(";")) params = params.substring(1); - String[] array = params.split(";"); - for (String param : array) - { - int pidx = param.indexOf("="); - String name = param.substring(0, pidx); - String val = param.substring(pidx + 1); - if (name.equals("q")) - { - header.q = Double.valueOf(val); - } - else header.parameters.put(name, val); - } - } - return header; - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (!(o instanceof QualifiedStringHeader)) return false; - - QualifiedStringHeader comp = (QualifiedStringHeader) o; - - if (!value.equals(comp.value)) return false; - - Map params1 = this.getParameters(); - Map params2 = comp.getParameters(); - - if (params1 == params2) return true; - if (params1 == null || params2 == null) return false; - if (params1.size() == 0 && params2.size() == 0) return true; - int numParams1 = params1.size(); - if (params1.containsKey("q")) numParams1--; - int numParams2 = params2.size(); - if (params2.containsKey("q")) numParams2--; - - if (numParams1 != numParams2) return false; - if (numParams1 == 0) return true; - - for (Map.Entry entry : params1.entrySet()) - { - String key = entry.getKey(); - if (key.equals("q")) continue; - String value = entry.getValue(); - String value2 = params2.get(key); - if (value == value2) continue; // both null - if (value == null || value2 == null) return false; - if (value.equals(value2) == false) return false; - } - return true; - } - - public int compareWeight(QualifiedStringHeader header) - { - if (q == header.q) return 0; - else if (q < header.q) return 1; - else return -1; - } - -} Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/AcceptableVariant.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/AcceptableVariant.java (revision 774) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/AcceptableVariant.java (working copy) @@ -1,137 +0,0 @@ -package org.jboss.resteasy.util; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Variant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class AcceptableVariant implements Comparable -{ - private Variant variant; - private MediaType type; - private QualifiedStringHeader language; - private QualifiedStringHeader encoding; - - public AcceptableVariant(Variant variant) - { - this.variant = variant; - this.type = variant.getMediaType(); - if (variant.getLanguage() != null) - { - language = new QualifiedStringHeader(LocaleHelper.toLanguageString(variant.getLanguage())); - } - if (variant.getEncoding() != null) - { - encoding = QualifiedStringHeader.parse(variant.getEncoding()); - } - } - - public AcceptableVariant(MediaType type, String language, String encoding) - { - final Locale locale = language == null ? null : LocaleHelper.extractLocale(language.toLowerCase()); - this.variant = new Variant(type, locale, encoding); - this.type = type; - if (language != null) this.language = QualifiedStringHeader.parse(language); - if (encoding != null) this.encoding = QualifiedStringHeader.parse(encoding); - - } - - public int compareTo(AcceptableVariant acceptableVariant) - { - int compare = 0; - if (type == acceptableVariant.type) compare = 0; - else if (type != null && acceptableVariant.type != null) - compare = MediaTypeHelper.compareWeight(type, acceptableVariant.type); - else if (type == null) compare = 1; - else if (type != null) compare = -1; - - if (compare != 0) return compare; - - if (language == acceptableVariant.language) compare = 0; - else if (language != null && acceptableVariant.language != null) - compare = language.compareWeight(acceptableVariant.language); - else if (language == null) compare = 1; - else if (language != null) compare = -1; - - - if (compare != 0) return compare; - - if (encoding == acceptableVariant.encoding) compare = 0; - else if (encoding != null && acceptableVariant.encoding != null) - compare = encoding.compareWeight(acceptableVariant.encoding); - else if (encoding == null) return 1; - else if (encoding != null) return -1; - - return compare; - } - - public Variant getVariant() - { - return variant; - } - - public MediaType getType() - { - return type; - } - - public QualifiedStringHeader getLanguage() - { - return language; - } - - public QualifiedStringHeader getEncoding() - { - return encoding; - } - - public static List sort(List acceptable) - { - Collections.sort(acceptable); - List sorted = new ArrayList(); - for (AcceptableVariant v : acceptable) - { - sorted.add(v.getVariant()); - } - return sorted; - } - - public static Variant pick(List has, List acceptable) - { - Collections.sort(acceptable); - - ArrayList produces = new ArrayList(); - for (Variant v : has) produces.add(new AcceptableVariant(v)); - - for (AcceptableVariant accept : acceptable) - { - for (AcceptableVariant produce : produces) - { - boolean match = false; - if (produce.getType() == null || accept.getType() == null) match = true; - else match = MediaTypeHelper.equivalent(produce.getType(), accept.getType()); - - if (!match) continue; - - match = false; - if (produce.getLanguage() == null || accept.getLanguage() == null) match = true; - else match = produce.getLanguage().equals(accept.getLanguage()); - - if (!match) continue; - - match = false; - if (produce.getEncoding() == null || accept.getEncoding() == null) match = true; - else match = produce.getEncoding().equals(accept.getEncoding()); - - if (match) return produce.getVariant(); - } - } - return null; - } -} Index: jaxrs/providers/jaxb/src/test/java/org/jboss/resteasy/test/providers/jaxb/CharacterSetTest.java =================================================================== --- jaxrs/providers/jaxb/src/test/java/org/jboss/resteasy/test/providers/jaxb/CharacterSetTest.java (revision 0) +++ jaxrs/providers/jaxb/src/test/java/org/jboss/resteasy/test/providers/jaxb/CharacterSetTest.java (revision 0) @@ -0,0 +1,137 @@ +package org.jboss.resteasy.test.providers.jaxb; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Variant; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + + +/** + @see RFC 2616 paragraph 14.2: "Accept-Charset" + @see RFC 1521 paragraph 7.1.1: "The charset parameter" + @author Pascal S. de Kloe + */ +@Path("/") +public class CharacterSetTest { + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final String[] characterSets = {"UTF-8", "UTF-16LE"}; + + private final DocumentBuilder documentBuilder; + + + @XmlRootElement(name="test") + public static class TestData { + private String text = "Some arbitrary text."; + + public String getText() { + return text; + } + + public void setText(String value) { + text = value; + } + } + + @GET + @Path("plain") + @Produces("application/xml") + public Response getPlain() { + return Response.ok(new TestData()).build(); + } + + @GET + @Path("variant") + @Produces("application/xml") + public Response getVariant(@Context Request request) { + int i = characterSets.length; + MediaType[] mediaTypes = new MediaType[i]; + while (--i >= 0) + mediaTypes[i] = MediaType.valueOf("application/xml;charset=" + characterSets[i]); + List variants = Variant.mediaTypes(mediaTypes).build(); + Variant variant = request.selectVariant(variants); + if (variant == null) + return Response.notAcceptable(variants).build(); + return Response.ok(new TestData(), variant).build(); + } + + public CharacterSetTest() throws ParserConfigurationException { + documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + dispatcher.getRegistry().addSingletonResource(this); + } + + @Test + public void plainCharset() throws URISyntaxException, SAXException, IOException { + assertCharset("/plain"); + } + + @Test + public void plainAccept() throws URISyntaxException, SAXException, IOException { + assertAccept("/plain"); + } + + @Test + public void variantCharset() throws URISyntaxException, SAXException, IOException { + assertCharset("/variant"); + } + + @Test + public void variantAccept() throws URISyntaxException, SAXException, IOException { + assertAccept("/variant"); + } + + + private void assertCharset(String path) throws URISyntaxException, SAXException, IOException { + for (String characterSet : characterSets) { + MockHttpRequest request = MockHttpRequest.get(path); + request.accept("application/xml"); + request.header("Accept-Charset", characterSet); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals("Status code.", 200, response.getStatus()); + assertXMLCharacterSet(characterSet, response); + } + } + + + private void assertAccept(String path) throws URISyntaxException, SAXException, IOException { + for (String characterSet : characterSets) { + MockHttpRequest request = MockHttpRequest.get(path); + request.accept("application/xml;charset=" + characterSet); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals("Status code.", 200, response.getStatus()); + assertXMLCharacterSet(characterSet, response); + } + } + + private void assertXMLCharacterSet(String characterSet, MockHttpResponse response) throws SAXException, IOException { + Document document = documentBuilder.parse(new ByteArrayInputStream(response.getOutput())); + assertEquals("XML encoding.", characterSet, document.getInputEncoding()); + } + +}