juneau-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jamesbog...@apache.org
Subject [2/3] incubator-juneau git commit: New Accept/AcceptEncoding/ContentType classes.
Date Tue, 02 May 2017 01:11:47 GMT
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroup.java b/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
index ce948c6..194a39f 100644
--- a/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
+++ b/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
@@ -15,7 +15,7 @@ package org.apache.juneau.encoders;
 import java.util.*;
 import java.util.concurrent.*;
 
-import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 
 /**
  * Represents the group of {@link Encoder encoders} keyed by codings.
@@ -53,9 +53,12 @@ import org.apache.juneau.*;
 public final class EncoderGroup {
 
 	// Maps Accept-Encoding headers to matching encoders.
-	private final Map<String,EncoderMatch> cache = new ConcurrentHashMap<String,EncoderMatch>();
+	private final ConcurrentHashMap<String,EncoderMatch> cache = new ConcurrentHashMap<String,EncoderMatch>();
 
-	final Encoder[] encoders;
+	private final String[] encodings;
+	private final List<String> encodingsList;
+	private final Encoder[] encodingsEncoders;
+	private final List<Encoder> encoders;
 
 	/**
 	 * Constructor
@@ -63,9 +66,21 @@ public final class EncoderGroup {
 	 * @param encoders The encoders to add to this group.
 	 */
 	public EncoderGroup(Encoder[] encoders) {
-		this.encoders = Arrays.copyOf(encoders, encoders.length);
-	}
+		this.encoders = Collections.unmodifiableList(new ArrayList<Encoder>(Arrays.asList(encoders)));
+
+		List<String> lc = new ArrayList<String>();
+		List<Encoder> l = new ArrayList<Encoder>();
+		for (Encoder e : encoders) {
+			for (String c: e.getCodings()) {
+				lc.add(c);
+				l.add(e);
+			}
+		}
 
+		this.encodings = lc.toArray(new String[lc.size()]);
+		this.encodingsList = Collections.unmodifiableList(lc);
+		this.encodingsEncoders = l.toArray(new Encoder[l.size()]);
+	}
 
 	/**
 	 * Returns the coding string for the matching encoder that can handle the specified <code>Accept-Encoding</code>
@@ -79,62 +94,47 @@ public final class EncoderGroup {
 	 * @return The coding value (e.g. <js>"gzip"</js>).
 	 */
 	public EncoderMatch getEncoderMatch(String acceptEncoding) {
-		if (encoders.length == 0)
-			return null;
-
 		EncoderMatch em = cache.get(acceptEncoding);
 		if (em != null)
 			return em;
 
-		MediaRange[] ae = MediaRange.parse(acceptEncoding);
-
-		if (ae.length == 0)
-			ae = MediaRange.parse("*/*");
-
-		Map<Float,EncoderMatch> m = null;
+		AcceptEncoding ae = AcceptEncoding.forString(acceptEncoding);
+		int match = ae.findMatch(encodings);
 
-		for (MediaRange a : ae) {
-			for (Encoder e : encoders) {
-				for (String c : e.getCodings()) {
-					MediaType mt = MediaType.forString(c);
-					float q = a.matches(mt);
-					if (q == 1) {
-						em = new EncoderMatch(mt, e);
-						cache.put(acceptEncoding, em);
-						return em;
-					} else if (q > 0) {
-						if (m == null)
-							m = new TreeMap<Float,EncoderMatch>(Collections.reverseOrder());
-						m.put(q, new EncoderMatch(mt, e));
-					}
-				}
-			}
+		if (match >= 0) {
+			em = new EncoderMatch(encodings[match], encodingsEncoders[match]);
+			cache.putIfAbsent(acceptEncoding, em);
 		}
-		return (m == null ? null : m.values().iterator().next());
+
+		return cache.get(acceptEncoding);
 	}
 
 	/**
 	 * Returns the encoder registered with the specified coding (e.g. <js>"gzip"</js>).
 	 *
-	 * @param coding The coding string.
+	 * @param encoding The coding string.
 	 * @return The encoder, or <jk>null</jk> if encoder isn't registered with that coding.
 	 */
-	public Encoder getEncoder(String coding) {
-		EncoderMatch em = getEncoderMatch(coding);
+	public Encoder getEncoder(String encoding) {
+		EncoderMatch em = getEncoderMatch(encoding);
 		return (em == null ? null : em.getEncoder());
 	}
 
 	/**
 	 * Returns the set of codings supported by all encoders in this group.
 	 *
-	 * @return The set of codings supported by all encoders in this group.  Never <jk>null</jk>.
+	 * @return An unmodifiable list of codings supported by all encoders in this group.  Never <jk>null</jk>.
 	 */
 	public List<String> getSupportedEncodings() {
-		List<String> l = new ArrayList<String>();
-		for (Encoder e : encoders)
-			for (String enc : e.getCodings())
-				if (! l.contains(enc))
-					l.add(enc);
-		return l;
+		return encodingsList;
+	}
+
+	/**
+	 * Returns the encoders in this group.
+	 *
+	 * @return An unmodifiable list of encoders in this group.
+	 */
+	public List<Encoder> getEncoders() {
+		return encoders;
 	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroupBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroupBuilder.java b/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroupBuilder.java
index 7a965c6..2180fd1 100644
--- a/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroupBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderGroupBuilder.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.encoders;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
 import java.util.*;
 
 /**
@@ -33,7 +34,8 @@ public class EncoderGroupBuilder {
 	 * @param copyFrom The encoder group that we're copying settings and encoders from.
 	 */
 	public EncoderGroupBuilder(EncoderGroup copyFrom) {
-		this.encoders = new ArrayList<Encoder>(Arrays.asList(copyFrom.encoders));
+		this.encoders = new ArrayList<Encoder>();
+		addReverse(encoders, copyFrom.getEncoders());
 	}
 
 	/**
@@ -43,9 +45,9 @@ public class EncoderGroupBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public EncoderGroupBuilder append(Class<?>...e) {
-		for (Class<?> ee : e) {
+		for (int i = e.length-1; i >= 0; i--) {
 			try {
-				encoders.add((Encoder)((Class<?>)ee).newInstance());
+				encoders.add((Encoder)((Class<?>)e[i]).newInstance());
 			} catch (Exception x) {
 				throw new RuntimeException(x);
 			}
@@ -60,7 +62,7 @@ public class EncoderGroupBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public EncoderGroupBuilder append(Encoder...e) {
-		encoders.addAll(Arrays.asList(e));
+		addReverse(encoders, e);
 		return this;
 	}
 
@@ -70,8 +72,8 @@ public class EncoderGroupBuilder {
 	 * @param e The encoders to append to this group.
 	 * @return This object (for method chaining).
 	 */
-	public EncoderGroupBuilder append(Collection<Encoder> e) {
-		encoders.addAll(e);
+	public EncoderGroupBuilder append(List<Encoder> e) {
+		addReverse(encoders, e);
 		return this;
 	}
 
@@ -82,7 +84,7 @@ public class EncoderGroupBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public EncoderGroupBuilder append(EncoderGroup eg) {
-		append(eg.encoders);
+		append(eg.getEncoders());
 		return this;
 	}
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderMatch.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderMatch.java b/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderMatch.java
index e05b3e7..f5ecb3d 100644
--- a/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderMatch.java
+++ b/juneau-core/src/main/java/org/apache/juneau/encoders/EncoderMatch.java
@@ -12,18 +12,16 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.encoders;
 
-import org.apache.juneau.*;
-
 /**
  * Represents a encoder and encoding that matches an HTTP <code>Accept-Encoding</code> header value.
  */
 public final class EncoderMatch {
 
-	private final MediaType mediaType;
+	private final String encoding;
 	private final Encoder encoder;
 
-	EncoderMatch(MediaType mediaType, Encoder encoder) {
-		this.mediaType = mediaType;
+	EncoderMatch(String encoding, Encoder encoder) {
+		this.encoding = encoding;
 		this.encoder = encoder;
 	}
 
@@ -33,7 +31,7 @@ public final class EncoderMatch {
 	 * @return The encoding of the match.
 	 */
 	public String getEncoding() {
-		return mediaType.getType();
+		return encoding;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
index b7030e5..d97afe4 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
@@ -18,6 +18,7 @@ import java.util.*;
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
 import org.apache.juneau.dto.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.serializer.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
index 440c80b..0e334d1 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
@@ -18,6 +18,7 @@ import java.lang.reflect.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.serializer.*;

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java
index 304bfab..2095968 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java
@@ -23,6 +23,7 @@ import javax.xml.stream.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.transform.*;

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserBuilder.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserBuilder.java
index be8cc9c..6c9f27c 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserBuilder.java
@@ -18,6 +18,7 @@ import javax.xml.stream.*;
 import javax.xml.stream.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.xml.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java
index 5ebef14..2bf5e25 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java
@@ -22,6 +22,7 @@ import java.util.*;
 import javax.xml.stream.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.xml.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
index eea6616..bc17387 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
@@ -20,6 +20,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.transform.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
index 168ac5c..c3d1088 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
@@ -21,6 +21,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.transform.*;
 import org.apache.juneau.xml.*;

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerBuilder.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerBuilder.java
index fe0a7c5..6190c14 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerBuilder.java
@@ -17,6 +17,7 @@ import static org.apache.juneau.html.HtmlSerializerContext.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.xml.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
index 4c1ee2f..3faf271 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
@@ -20,6 +20,7 @@ import java.util.*;
 import java.util.regex.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.xml.*;

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/http/Accept.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/http/Accept.java b/juneau-core/src/main/java/org/apache/juneau/http/Accept.java
new file mode 100644
index 0000000..ce7119f
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/http/Accept.java
@@ -0,0 +1,231 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.http;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents a parsed <code>Accept:</code> HTTP header.
+ * <p>
+ * The formal RFC2616 header field definition is as follows:
+ * <p class='bcode'>
+ * 	14.1 Accept
+ *
+ * 	The Accept request-header field can be used to specify certain media
+ * 	types which are acceptable for the response. Accept headers can be
+ * 	used to indicate that the request is specifically limited to a small
+ * 	set of desired types, as in the case of a request for an in-line
+ * 	image.
+ *
+ * 	 Accept         = "Accept" ":
+ * 							#( media-range [ accept-params ] )
+ *
+ * 	 media-range    = ( "* /*"
+ * 							| ( type "/" "*" )
+ * 							| ( type "/" subtype )
+ * 							) *( ";" parameter )
+ * 	 accept-params  = ";" "q" "=" qvalue *( accept-extension )
+ * 	 accept-extension = ";" token [ "=" ( token | quoted-string ) ]
+ *
+ * 	The asterisk "*" character is used to group media types into ranges,
+ * 	with "* /*" indicating all media types and "type/*" indicating all
+ * 	subtypes of that type. The media-range MAY include media type
+ * 	parameters that are applicable to that range.
+ *
+ * 	Each media-range MAY be followed by one or more accept-params,
+ * 	beginning with the "q" parameter for indicating a relative quality
+ * 	factor. The first "q" parameter (if any) separates the media-range
+ * 	parameter(s) from the accept-params. Quality factors allow the user
+ * 	or user agent to indicate the relative degree of preference for that
+ * 	media-range, using the qvalue scale from 0 to 1 (section 3.9). The
+ * 	default value is q=1.
+ *
+ * 	Note: Use of the "q" parameter name to separate media type
+ * 	parameters from Accept extension parameters is due to historical
+ * 	practice. Although this prevents any media type parameter named
+ * 	"q" from being used with a media range, such an event is believed
+ * 	to be unlikely given the lack of any "q" parameters in the IANA
+ * 	media type registry and the rare usage of any media type
+ * 	parameters in Accept. Future media types are discouraged from
+ * 	registering any parameter named "q".
+ *
+ * 	The example
+ *
+ * 		Accept: audio/*; q=0.2, audio/basic
+ *
+ * 	SHOULD be interpreted as "I prefer audio/basic, but send me any audio
+ * 	type if it is the best available after an 80% mark-down in quality."
+ *
+ * 	If no Accept header field is present, then it is assumed that the
+ * 	client accepts all media types. If an Accept header field is present,
+ * 	and if the server cannot send a response which is acceptable
+ * 	according to the combined Accept field value, then the server SHOULD
+ * 	send a 406 (not acceptable) response.
+ *
+ * 	A more elaborate example is
+ *
+ * 	    Accept: text/plain; q=0.5, text/html,
+ * 	            text/x-dvi; q=0.8, text/x-c
+ *
+ * 	Verbally, this would be interpreted as "text/html and text/x-c are
+ * 	the preferred media types, but if they do not exist, then send the
+ * 	text/x-dvi entity, and if that does not exist, send the text/plain
+ * 	entity."
+ *
+ * 	Media ranges can be overridden by more specific media ranges or
+ * 	specific media types. If more than one media range applies to a given
+ * 	type, the most specific reference has precedence. For example,
+ *
+ * 	    Accept: text/ *, text/html, text/html;level=1, * /*
+ *
+ * 	have the following precedence:
+ *
+ * 	    1) text/html;level=1
+ * 	    2) text/html
+ * 	    3) text/*
+ * 	    4) * /*
+ *
+ * 	The media type quality factor associated with a given type is
+ * 	determined by finding the media range with the highest precedence
+ * 	which matches that type. For example,
+ *
+ * 	    Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1,
+ * 	            text/html;level=2;q=0.4, * /*;q=0.5
+ *
+ * 	would cause the following values to be associated:
+ *
+ * 	    text/html;level=1         = 1
+ * 	    text/html                 = 0.7
+ * 	    text/plain                = 0.3
+ * 	    image/jpeg                = 0.5
+ * 	    text/html;level=2         = 0.4
+ * 	    text/html;level=3         = 0.7
+ *
+ * 	   Note: A user agent might be provided with a default set of quality
+ * 	   values for certain media ranges. However, unless the user agent is
+ * 	   a closed system which cannot interact with other rendering agents,
+ * 	   this default set ought to be configurable by the user.
+ * </p>
+ */
+public final class Accept {
+
+	private static final boolean nocache = Boolean.getBoolean("juneau.nocache");
+	private static final ConcurrentHashMap<String,Accept> cache = new ConcurrentHashMap<String,Accept>();
+
+	private final MediaTypeRange[] mediaRanges;
+	private final List<MediaTypeRange> mediaRangesList;
+
+	/**
+	 * Returns a parsed <code>Accept</code> header.
+	 *
+	 * @param s The <code>Accept</code> header string.
+	 * @return The parsed <code>Accept</code> header.
+	 */
+	public static Accept forString(String s) {
+		if (s == null)
+			s = "null";
+		Accept a = cache.get(s);
+		if (a == null) {
+			a = new Accept(s);
+			if (nocache)
+				return a;
+			cache.putIfAbsent(s, a);
+		}
+		return cache.get(s);
+	}
+
+	private Accept(String raw) {
+		this.mediaRanges = MediaTypeRange.parse(raw);
+		this.mediaRangesList = Collections.unmodifiableList(Arrays.asList(mediaRanges));
+	}
+
+	/**
+	 * Returns the list of the media ranges that make up this header.
+	 * <p>
+	 * The media ranges in the list are sorted by their q-value in descending order.
+	 *
+	 * @return An unmodifiable list of media ranges.
+	 */
+	public List<MediaTypeRange> getMediaRanges() {
+		return mediaRangesList;
+	}
+
+	/**
+	 * Given a list of media types, returns the best match for this <code>Accept</code> header.
+	 * <p>
+	 * Note that fuzzy matching is allowed on the media types where the <code>Accept</code> header may
+	 * contain additional subtype parts.
+	 * <br>For example, given identical q-values and an <code>Accept</code> value of <js>"text/json+activity"</js>,
+	 * the media type <js>"text/json"</js> will match if <js>"text/json+activity"</js> or <js>"text/activity+json"</js>
+	 * isn't found.
+	 * <br>The purpose for this is to allow serializers to match when artifacts such as <code>id</code> properties are present
+	 * in the header.
+	 * <p>
+	 * See <a class='doclink' href='https://www.w3.org/TR/activitypub/#retrieving-objects'>ActivityPub / Retrieving Objects</a>
+	 * <p>
+	 *
+	 * @param mediaTypes The media types to match against.
+	 * @return The index into the array of the best match, or <code>-1</code> if no suitable matches could be found.
+	 */
+	public int findMatch(MediaType[] mediaTypes) {
+		int matchQuant = 0, matchIndex = -1;
+		float q = 0f;
+
+		// Media ranges are ordered by 'q'.
+		// So we only need to search until we've found a match.
+		for (MediaTypeRange mr : mediaRanges) {
+			float q2 = mr.getQValue();
+
+			if (q2 < q || q2 == 0)
+				break;
+
+			for (int i = 0; i < mediaTypes.length; i++) {
+				MediaType mt = mediaTypes[i];
+				int matchQuant2 = mt.match(mr.getMediaType());
+				if (matchQuant2 > matchQuant) {
+					matchIndex = i;
+					matchQuant = matchQuant2;
+					q = q2;
+				}
+			}
+		}
+
+		return matchIndex;
+	}
+
+	/**
+	 * Convenience method for searching through all of the subtypes of all the media ranges in this header
+	 * for the presence of a subtype fragment.
+	 * <p>
+	 * For example, given the header <js>"text/json+activity"</js>, calling <code>hasSubtypePart(<js>"activity"</js>)</code> returns <jk>true</jk>.
+	 *
+	 * @param part The media type subtype fragment.
+	 * @return <jk>true</jk> if subtype fragement exists.
+	 */
+	public boolean hasSubtypePart(String part) {
+
+		for (MediaTypeRange mr : this.mediaRanges)
+			if (mr.getQValue() > 0 && mr.getMediaType().getSubTypes().indexOf(part) >= 0)
+				return true;
+
+		return false;
+	}
+
+	@Override /* Object */
+	public String toString() {
+		return StringUtils.join(mediaRanges, ',');
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/http/AcceptEncoding.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/http/AcceptEncoding.java b/juneau-core/src/main/java/org/apache/juneau/http/AcceptEncoding.java
new file mode 100644
index 0000000..e8c6886
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/http/AcceptEncoding.java
@@ -0,0 +1,147 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.http;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents a parsed <code>Accept-Encoding:</code> HTTP header.
+ * <p>
+ * The formal RFC2616 header field definition is as follows:
+ * <p class='bcode'>
+ * 	The Accept-Encoding request-header field is similar to Accept, but restricts
+ * 	the content-codings (section 3.5) that are acceptable in the response.
+ *
+ * 		Accept-Encoding  = "Accept-Encoding" ":"
+ * 		                   1#( codings [ ";" "q" "=" qvalue ] )
+ * 		codings          = ( content-coding | "*" )
+ *
+ * 	Examples of its use are:
+ *
+ * 		Accept-Encoding: compress, gzip
+ * 		Accept-Encoding:
+ * 		Accept-Encoding: *
+ * 		Accept-Encoding: compress;q=0.5, gzip;q=1.0
+ * 		Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
+ *
+ * 	A server tests whether a content-coding is acceptable, according to an
+ * 	Accept-Encoding field, using these rules:
+ *
+ * 	   1. If the content-coding is one of the content-codings listed in
+ * 	      the Accept-Encoding field, then it is acceptable, unless it is
+ * 	      accompanied by a qvalue of 0. (As defined in section 3.9, a
+ * 	      qvalue of 0 means "not acceptable.")
+ * 	   2. The special "*" symbol in an Accept-Encoding field matches any
+ * 	      available content-coding not explicitly listed in the header
+ * 	      field.
+ * 	   3. If multiple content-codings are acceptable, then the acceptable
+ * 	      content-coding with the highest non-zero qvalue is preferred.
+ * 	   4. The "identity" content-coding is always acceptable, unless
+ * 	      specifically refused because the Accept-Encoding field includes
+ * 	      "identity;q=0", or because the field includes "*;q=0" and does
+ * 	      not explicitly include the "identity" content-coding. If the
+ * 	      Accept-Encoding field-value is empty, then only the "identity"
+ * 	      encoding is acceptable.
+ *
+ * 	If an Accept-Encoding field is present in a request, and if the server cannot
+ * 	send a response which is acceptable according to the Accept-Encoding header,
+ * 	then the server SHOULD send an error response with the 406 (Not Acceptable) status code.
+ *
+ * 	If no Accept-Encoding field is present in a request, the server MAY assume
+ * 	that the client will accept any content coding. In this case, if "identity"
+ * 	is one of the available content-codings, then the server SHOULD use the "identity"
+ * 	content-coding, unless it has additional information that a different content-coding
+ * 	is meaningful to the client.
+ *
+ * 	      Note: If the request does not include an Accept-Encoding field,
+ * 	      and if the "identity" content-coding is unavailable, then
+ * 	      content-codings commonly understood by HTTP/1.0 clients (i.e.,
+ * 	      "gzip" and "compress") are preferred; some older clients
+ * 	      improperly display messages sent with other content-codings.  The
+ * 	      server might also make this decision based on information about
+ * 	      the particular user-agent or client.
+ * 	      Note: Most HTTP/1.0 applications do not recognize or obey qvalues
+ * 	      associated with content-codings. This means that qvalues will not
+ * 	      work and are not permitted with x-gzip or x-compress.
+ * </p>
+ */
+public final class AcceptEncoding {
+
+	private static final boolean nocache = Boolean.getBoolean("juneau.nocache");
+	private static final ConcurrentHashMap<String,AcceptEncoding> cache = new ConcurrentHashMap<String,AcceptEncoding>();
+
+	private final TypeRange[] typeRanges;
+	private final List<TypeRange> typeRangesList;
+
+	/**
+	 * Returns a parsed <code>Accept-Encoding</code> header.
+	 *
+	 * @param s The <code>Accept-Encoding</code> header string.
+	 * @return The parsed <code>Accept-Encoding</code> header.
+	 */
+	public static AcceptEncoding forString(String s) {
+		if (s == null)
+			s = "null";
+		AcceptEncoding a = cache.get(s);
+		if (a == null) {
+			a = new AcceptEncoding(s);
+			if (nocache)
+				return a;
+			cache.putIfAbsent(s, a);
+		}
+		return cache.get(s);
+	}
+
+	private AcceptEncoding(String raw) {
+		this.typeRanges = TypeRange.parse(raw);
+		this.typeRangesList = Collections.unmodifiableList(Arrays.asList(typeRanges));
+	}
+
+	/**
+	 * Returns the list of the types ranges that make up this header.
+	 * <p>
+	 * The types ranges in the list are sorted by their q-value in descending order.
+	 *
+	 * @return An unmodifiable list of type ranges.
+	 */
+	public List<TypeRange> getTypeRanges() {
+		return typeRangesList;
+	}
+
+	/**
+	 * Given a list of content codings, returns the best match for this <code>Accept-Encoding</code> header.
+	 * <p>
+	 *
+	 * @param contentCodings The codings to match against.
+	 * @return The index into the array of the best match, or <code>-1</code> if no suitable matches could be found.
+	 */
+	public int findMatch(String[] contentCodings) {
+
+		// Type ranges are ordered by 'q'.
+		// So we only need to search until we've found a match.
+		for (TypeRange mr : typeRanges)
+			for (int i = 0; i < contentCodings.length; i++)
+				if (mr.matches(contentCodings[i]))
+					return i;
+
+		return -1;
+	}
+
+	@Override /* Object */
+	public String toString() {
+		return StringUtils.join(typeRanges, ',');
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/http/ContentType.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/http/ContentType.java b/juneau-core/src/main/java/org/apache/juneau/http/ContentType.java
new file mode 100644
index 0000000..7f5570f
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/http/ContentType.java
@@ -0,0 +1,90 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.http;
+
+import java.util.concurrent.*;
+
+/**
+ * Represents a parsed <code>Content-Type:</code> HTTP header.
+ * <p>
+ * The formal RFC2616 header field definition is as follows:
+ * <p class='bcode'>
+ * 	14.17 Content-Type
+ *
+ * 	The Content-Type entity-header field indicates the media type of the
+ * 	entity-body sent to the recipient or, in the case of the HEAD method,
+ * 	the media type that would have been sent had the request been a GET.
+ *
+ * 		Content-Type   = "Content-Type" ":" media-type
+ *
+ * 	Media types are defined in section 3.7. An example of the field is
+ *
+ * 	Content-Type: text/html; charset=ISO-8859-4
+ * </p>
+ */
+public class ContentType extends MediaType {
+
+	private static final boolean nocache = Boolean.getBoolean("juneau.nocache");
+	private static final ConcurrentHashMap<String,ContentType> cache = new ConcurrentHashMap<String,ContentType>();
+
+	/**
+	 * Returns a parsed <code>Content-Type</code> header.
+	 *
+	 * @param s The <code>Content-Type</code> header string.
+	 * @return The parsed <code>Content-Type</code> header.
+	 */
+	public static ContentType forString(String s) {
+		if (s == null)
+			return null;
+		ContentType mt = cache.get(s);
+		if (mt == null) {
+			mt = new ContentType(s);
+			if (nocache)
+				return mt;
+			cache.putIfAbsent(s, mt);
+		}
+		return cache.get(s);
+	}
+
+	private ContentType(String s) {
+		super(s);
+	}
+
+	/**
+	 * Given a list of media types, returns the best match for this <code>Content-Type</code> header.
+	 * <p>
+	 * Note that fuzzy matching is allowed on the media types where the <code>Content-Types</code> header may
+	 * contain additional subtype parts.
+	 * <br>For example, given a <code>Content-Type</code> value of <js>"text/json+activity"</js>,
+	 * the media type <js>"text/json"</js> will match if <js>"text/json+activity"</js> or <js>"text/activity+json"</js>
+	 * isn't found.
+	 * <br>The purpose for this is to allow parsers to match when artifacts such as <code>id</code> properties are present
+	 * in the header.
+	 *
+	 * @param mediaTypes The media types to match against.
+	 * @return The index into the array of the best match, or <code>-1</code> if no suitable matches could be found.
+	 */
+	public int findMatch(MediaType[] mediaTypes) {
+		int matchQuant = 0, matchIndex = -1;
+
+		for (int i = 0; i < mediaTypes.length; i++) {
+			MediaType mt = mediaTypes[i];
+			int matchQuant2 = mt.match(this);
+			if (matchQuant2 > matchQuant) {
+				matchIndex = i;
+			}
+		}
+
+		return matchIndex;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/http/MediaType.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/http/MediaType.java b/juneau-core/src/main/java/org/apache/juneau/http/MediaType.java
new file mode 100644
index 0000000..74f298f
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/http/MediaType.java
@@ -0,0 +1,277 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.http;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+
+
+/**
+ * Describes a single media type used in content negotiation between an HTTP client and server, as described in
+ * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
+ */
+@BeanIgnore
+@SuppressWarnings("unchecked")
+public class MediaType {
+
+	private static final boolean nocache = Boolean.getBoolean("juneau.nocache");
+	private static final ConcurrentHashMap<String,MediaType> cache = new ConcurrentHashMap<String,MediaType>();
+
+	/** Reusable predefined media type */
+	@SuppressWarnings("javadoc")
+	public static final MediaType
+		CSV = forString("text/csv"),
+		HTML = forString("text/html"),
+		JSON = forString("application/json"),
+		MSGPACK = forString("octal/msgpack"),
+		PLAIN = forString("text/plain"),
+		UON = forString("text/uon"),
+		URLENCODING = forString("application/x-www-form-urlencoded"),
+		XML = forString("text/xml"),
+		XMLSOAP = forString("text/xml+soap"),
+
+		RDF = forString("text/xml+rdf"),
+		RDFABBREV = forString("text/xml+rdf+abbrev"),
+		NTRIPLE = forString("text/n-triple"),
+		TURTLE = forString("text/turtle"),
+		N3 = forString("text/n3")
+	;
+
+	private final String mediaType;
+	private final String type;								     // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset)
+	private final String subType;                        // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
+	private final String[] subTypes;                     // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
+	private final List<String> subTypesList;             // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
+	private final Map<String,Set<String>> parameters;    // The media type parameters (e.g. "text/html;level=1").  Does not include q!
+
+
+	/**
+	 * Returns the media type for the specified string.
+	 * The same media type strings always return the same objects so that these objects
+	 * can be compared for equality using '=='.
+	 * <p>
+	 * <h5 class='section'>Notes:</h5>
+	 * <ul>
+	 * 	<li>Spaces are replaced with <js>'+'</js> characters.
+	 * 		This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters
+	 * 		get replaced with spaces by your browser.  Since spaces aren't supported by the spec, this
+	 * 		is doesn't break anything.
+	 * 	<li>Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
+	 * </ul>
+	 *
+	 * @param s The media type string.  Will be lowercased.
+	 * 	<br>Returns <jk>null</jk> if input is null.
+	 * @return A cached media type object.
+	 */
+	public static MediaType forString(String s) {
+		if (s == null)
+			return null;
+		MediaType mt = cache.get(s);
+		if (mt == null) {
+			mt = new MediaType(s);
+			if (nocache)
+				return mt;
+			cache.putIfAbsent(s, mt);
+		}
+		return cache.get(s);
+	}
+
+	MediaType(String mt) {
+		Builder b = new Builder(mt);
+		this.mediaType = b.mediaType;
+		this.type = b.type;
+		this.subType = b.subType;
+		this.subTypes = b.subTypes;
+		this.subTypesList = Collections.unmodifiableList(Arrays.asList(subTypes));
+		this.parameters = (b.parameters == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.parameters));
+	}
+
+	private static class Builder {
+		private String mediaType, type, subType;
+		private String[] subTypes;
+		private Map<String,Set<String>> parameters;
+
+		private Builder(String mt) {
+			mt = mt.trim();
+
+			int i = mt.indexOf(';');
+			if (i == -1) {
+				this.parameters = Collections.EMPTY_MAP;
+			} else {
+				this.parameters = new TreeMap<String,Set<String>>();
+				String[] tokens = mt.substring(i+1).split(";");
+
+				for (int j = 0; j < tokens.length; j++) {
+					String[] parm = tokens[j].split("=");
+					if (parm.length == 2) {
+						String k = parm[0].trim(), v = parm[1].trim();
+						if (! parameters.containsKey(k))
+							parameters.put(k, new TreeSet<String>());
+						parameters.get(k).add(v);
+					}
+				}
+
+				mt = mt.substring(0, i);
+			}
+
+			this.mediaType = mt;
+			if (mt != null) {
+				mt = mt.replace(' ', '+');
+				i = mt.indexOf('/');
+				type = (i == -1 ? mt : mt.substring(0, i));
+				subType = (i == -1 ? "*" : mt.substring(i+1));
+			}
+			this.subTypes = StringUtils.split(subType, '+');
+		}
+	}
+
+	/**
+	 * Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string.
+	 *
+	 * @return The media type.
+	 */
+	public String getType() {
+		return type;
+	}
+
+	/**
+	 * Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string.
+	 *
+	 * @return The media subtype.
+	 */
+	public String getSubType() {
+		return subType;
+	}
+
+	/**
+	 * Returns the subtypes broken down by fragments delimited by <js>"'"</js>.
+	 * For example, the media type <js>"text/foo+bar"</js> will return a list of
+	 * <code>[<js>'foo'</js>,<js>'bar'</js>]</code>
+	 *
+	 * @return An unmodifiable list of subtype fragments.  Never <jk>null</jk>.
+	 */
+	public List<String> getSubTypes() {
+		return subTypesList;
+	}
+
+	/**
+	 * Returns <jk>true</jk> if this media type is a match for the specified media type.
+	 * <p>
+	 * Matches if any of the following is true:
+	 * <ul>
+	 * 	<li>Both type and subtype are the same.
+	 * 	<li>One or both types are <js>'*'</js> and the subtypes are the same.
+	 * 	<li>One or both subtypes are <js>'*'</js> and the types are the same.
+	 * 	<li>Either is <js>'*\/*'</js>.
+	 * </ul>
+	 *
+	 * @param o The media type to compare with.
+	 * @return <jk>true</jk> if the media types match.
+	 */
+	public final boolean matches(MediaType o) {
+		return match(o) > 0;
+	}
+
+	/**
+	 * Returns a match metric against the specified media type where a larger number represents a better match.
+	 * <p>
+	 * <ul>
+	 * 	<li>Exact matches (e.g. <js>"text/json"<js>/</js>"text/json"</js>) should match
+	 * 		better than metacharacter matches (e.g. <js>"text/*"<js>/</js>"text/json"</js>)
+	 * 	<li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>)
+	 * 		that will not prevent a match.  The reverse is not true, e.g. the comparison media type
+	 * 		must contain all subtype tokens found in the comparing media type.
+	 * 		<ul>
+	 * 			<li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>.
+	 * 			<li>We want to make sure {@link org.apache.juneau.json.JsonSerializer.Simple} (<js>"text/json+simple"</js>) does not handle
+	 * 				requests for <js>"text/json"</js>.
+	 * 		</ul>
+	 * 		More token matches should result in a higher match number.
+	 * </ul>
+	 *
+	 * @param o The media type to compare with.
+	 * @return <jk>true</jk> if the media types match.
+	 */
+	public final int match(MediaType o) {
+
+		// Perfect match
+		if (this == o || (type.equals(o.type) && subType.equals(o.subType)))
+			return Integer.MAX_VALUE;
+
+		int c1 = 0, c2 = 0;
+
+		if (type.equals(o.type))
+			c1 += 10000;
+		else if ("*".equals(type) || "*".equals(o.type))
+			c1 += 5000;
+
+		if (c1 == 0)
+			return 0;
+
+		// Give type slightly higher comparison value than subtype simply for deterministic results.
+		if (subType.equals(o.subType))
+			return c1 + 9999;
+
+		int c3 = 0;
+
+		for (String st1 : subTypes) {
+			if ("*".equals(st1))
+				c1++;
+			else if (ArrayUtils.contains(st1, o.subTypes))
+				c1 += 100;
+			else if (ArrayUtils.contains("*", o.subTypes))
+				c1 += 10;
+			else
+				return 0;
+		}
+
+		return c1 + c2 + c3;
+	}
+
+	/**
+	 * Returns the additional parameters on this media type.
+	 * <p>
+	 * For example, given the media type string <js>"text/html;level=1"</js>, will return a map
+	 * with the single entry <code>{level:[<js>'1'</js>]}</code>.
+	 *
+	 * @return The map of additional parameters, or an empty map if there are no parameters.
+	 */
+	public Map<String,Set<String>> getParameters() {
+		return parameters;
+	}
+
+	@Override /* Object */
+	public String toString() {
+		if (parameters.isEmpty())
+			return mediaType;
+		StringBuilder sb = new StringBuilder(mediaType);
+		for (Map.Entry<String,Set<String>> e : parameters.entrySet())
+			for (String value : e.getValue())
+				sb.append(';').append(e.getKey()).append('=').append(value);
+		return sb.toString();
+	}
+
+	@Override /* Object */
+	public int hashCode() {
+		return mediaType.hashCode();
+	}
+
+	@Override /* Object */
+	public boolean equals(Object o) {
+		return this == o;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/http/MediaTypeRange.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/http/MediaTypeRange.java b/juneau-core/src/main/java/org/apache/juneau/http/MediaTypeRange.java
new file mode 100644
index 0000000..9ff7c4a
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/http/MediaTypeRange.java
@@ -0,0 +1,270 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.http;
+
+import java.util.*;
+import java.util.Map.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Describes a single type used in content negotiation between an HTTP client and server, as described in
+ * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
+ */
+@BeanIgnore
+public final class MediaTypeRange implements Comparable<MediaTypeRange>  {
+
+	private static final MediaTypeRange[] DEFAULT = new MediaTypeRange[]{new MediaTypeRange("*/*")};
+
+	private final MediaType mediaType;
+	private final Float qValue;
+	private final Map<String,Set<String>> extensions;
+
+	/**
+	 * Parses an <code>Accept</code> header value into an array of media ranges.
+	 * <p>
+	 * The returned media ranges are sorted such that the most acceptable media is available at ordinal position <js>'0'</js>, and the least acceptable at position n-1.
+	 * <p>
+	 * The syntax expected to be found in the referenced <code>value</code> complies with the syntax described in RFC2616, Section 14.1, as described below:
+	 * <p class='bcode'>
+	 * 	Accept         = "Accept" ":"
+	 * 	                  #( media-range [ accept-params ] )
+	 *
+	 * 	media-range    = ( "*\/*"
+	 * 	                  | ( type "/" "*" )
+	 * 	                  | ( type "/" subtype )
+	 * 	                  ) *( ";" parameter )
+	 * 	accept-params  = ";" "q" "=" qvalue *( accept-extension )
+	 * 	accept-extension = ";" token [ "=" ( token | quoted-string ) ]
+	 * </p>
+	 *
+	 * @param value The value to parse.  If <jk>null</jk> or empty, returns a single <code>MediaTypeRange</code> is returned that represents all types.
+	 * @return The media ranges described by the string.
+	 * The ranges are sorted such that the most acceptable media is available at ordinal position <js>'0'</js>, and the least acceptable at position n-1.
+	 */
+	public static MediaTypeRange[] parse(String value) {
+
+		if (value == null || value.length() == 0)
+			return DEFAULT;
+
+		if (value.indexOf(',') == -1)
+			return new MediaTypeRange[]{new MediaTypeRange(value)};
+
+		Set<MediaTypeRange> ranges = new TreeSet<MediaTypeRange>();
+
+		for (String r : StringUtils.split(value, ',')) {
+			r = r.trim();
+
+			if (r.isEmpty())
+				continue;
+
+			ranges.add(new MediaTypeRange(r));
+		}
+
+		return ranges.toArray(new MediaTypeRange[ranges.size()]);
+	}
+
+	@SuppressWarnings("unchecked")
+	private MediaTypeRange(String token) {
+		Builder b = new Builder(token);
+		this.mediaType = b.mediaType;
+		this.qValue = b.qValue;
+		this.extensions = (b.extensions == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.extensions));
+	}
+
+	private static class Builder {
+		private MediaType mediaType;
+		private Float qValue = 1f;
+		private Map<String,Set<String>> extensions;
+
+		private Builder(String token) {
+
+			token = token.trim();
+
+			int i = token.indexOf(";q=");
+
+			if (i == -1) {
+				mediaType = MediaType.forString(token);
+				return;
+			}
+
+			mediaType = MediaType.forString(token.substring(0, i));
+
+			String[] tokens = token.substring(i+1).split(";");
+
+			// Only the type of the range is specified
+			if (tokens.length > 0) {
+				boolean isInExtensions = false;
+				for (int j = 0; j < tokens.length; j++) {
+					String[] parm = tokens[j].split("=");
+					if (parm.length == 2) {
+						String k = parm[0], v = parm[1];
+						if (isInExtensions) {
+							if (extensions == null)
+								extensions = new TreeMap<String,Set<String>>();
+							if (! extensions.containsKey(k))
+								extensions.put(k, new TreeSet<String>());
+							extensions.get(k).add(v);
+						} else if (k.equals("q")) {
+							qValue = new Float(v);
+							isInExtensions = true;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Returns the media type enclosed by this media range.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <ul>
+	 * 	<li><js>"text/html"</js>
+	 * 	<li><js>"text/*"</js>
+	 * 	<li><js>"*\/*"</js>
+	 * </ul>
+	 *
+	 * @return The media type of this media range, lowercased, never <jk>null</jk>.
+	 */
+	public MediaType getMediaType() {
+		return mediaType;
+	}
+
+	/**
+	 * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616.
+	 * <p>
+	 * The quality value is a float between <code>0.0</code> (unacceptable) and <code>1.0</code> (most acceptable).
+	 * <p>
+	 * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js> header, as opposed to <js>"accept-*"</js>
+	 * header, its value will always be <js>"1"</js>.
+	 *
+	 * @return The 'q' value for this type, never <jk>null</jk>.
+	 */
+	public Float getQValue() {
+		return qValue;
+	}
+
+	/**
+	 * Returns the optional set of custom extensions defined for this type.
+	 * <p>
+	 * Values are lowercase and never <jk>null</jk>.
+	 *
+	 * @return The optional list of extensions, never <jk>null</jk>.
+	 */
+	public Map<String,Set<String>> getExtensions() {
+		return extensions;
+	}
+
+	/**
+	 * Provides a string representation of this media range, suitable for use as an <code>Accept</code> header value.
+	 * <p>
+	 * The literal text generated will be all lowercase.
+	 *
+	 * @return A media range suitable for use as an Accept header value, never <code>null</code>.
+	 */
+	@Override /* Object */
+	public String toString() {
+		StringBuffer sb = new StringBuffer().append(mediaType);
+
+		// '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue.
+		if (qValue.floatValue() == 1.0) {
+			if (! extensions.isEmpty()) {
+				sb.append(";q=").append(qValue);
+				for (Entry<String,Set<String>> e : extensions.entrySet()) {
+					String k = e.getKey();
+					for (String v : e.getValue())
+						sb.append(';').append(k).append('=').append(v);
+				}
+			}
+		} else {
+			sb.append(";q=").append(qValue);
+			for (Entry<String,Set<String>> e : extensions.entrySet()) {
+				String k = e.getKey();
+				for (String v : e.getValue())
+					sb.append(';').append(k).append('=').append(v);
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the specified object is also a <code>MediaType</code>, and has the same qValue, type, parameters, and extensions.
+	 *
+	 * @return <jk>true</jk> if object is equivalent.
+	 */
+	@Override /* Object */
+	public boolean equals(Object o) {
+
+		if (o == null || !(o instanceof MediaTypeRange))
+			return false;
+
+		if (this == o)
+			return true;
+
+		MediaTypeRange o2 = (MediaTypeRange) o;
+		return qValue.equals(o2.qValue)
+			&& mediaType.equals(o2.mediaType)
+			&& extensions.equals(o2.extensions);
+	}
+
+	/**
+	 * Returns a hash based on this instance's <code>media-type</code>.
+	 *
+	 * @return A hash based on this instance's <code>media-type</code>.
+	 */
+	@Override /* Object */
+	public int hashCode() {
+		return mediaType.hashCode();
+	}
+
+	/**
+	 * Compares two MediaRanges for equality.
+	 * <p>
+	 * The values are first compared according to <code>qValue</code> values.
+	 * Should those values be equal, the <code>type</code> is then lexicographically compared (case-insensitive) in ascending order,
+	 * 	with the <js>"*"</js> type demoted last in that order.
+	 * <code>MediaRanges</code> with the same type but different sub-types are compared - a more specific subtype is
+	 * 	promoted over the 'wildcard' subtype.
+	 * <code>MediaRanges</code> with the same types but with extensions are promoted over those same types with no extensions.
+	 *
+	 * @param o The range to compare to.  Never <jk>null</jk>.
+	 */
+	@Override /* Comparable */
+	public int compareTo(MediaTypeRange o) {
+
+		// Compare q-values.
+		int qCompare = Float.compare(o.qValue, qValue);
+		if (qCompare != 0)
+			return qCompare;
+
+		// Compare media-types.
+		// Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison.
+		int i = o.mediaType.toString().compareTo(mediaType.toString());
+		return i;
+	}
+
+	/**
+	 * Matches the specified media type against this range and returns a q-value
+	 * between 0 and 1 indicating the quality of the match.
+	 *
+	 * @param o The media type to match against.
+	 * @return A float between 0 and 1.  1 is a perfect match.  0 is no match at all.
+	 */
+	public float matches(MediaType o) {
+		if (this.mediaType == o || mediaType.matches(o))
+			return qValue;
+		return 0;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/http/TypeRange.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/http/TypeRange.java b/juneau-core/src/main/java/org/apache/juneau/http/TypeRange.java
new file mode 100644
index 0000000..a05189f
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/http/TypeRange.java
@@ -0,0 +1,276 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.http;
+
+import java.util.*;
+import java.util.Map.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents a single value in a comma-delimited header value that optionally contains a quality
+ * metric for comparison and extension parameters.
+ * <p>
+ * Similar in concept to {@link MediaTypeRange} except instead of media types (e.g. <js>"text/json"</js>),
+ * it's a simple type (e.g. <js>"iso-8601"</js>).
+ * <p>
+ * An example of a type range is a value in an <code>Accept-Encoding</code> header.
+ */
+@BeanIgnore
+public final class TypeRange implements Comparable<TypeRange>  {
+
+	private static final TypeRange[] DEFAULT = new TypeRange[]{new TypeRange("*")};
+
+	private final String type;
+	private final Float qValue;
+	private final Map<String,Set<String>> extensions;
+
+	/**
+	 * Parses a header such as an <code>Accept-Encoding</code> header value into an array of type ranges.
+	 * <p>
+	 * The syntax expected to be found in the referenced <code>value</code> complies with the syntax described in RFC2616, Section 14.1, as described below:
+	 * <p class='bcode'>
+	 * 	Accept-Encoding  = "Accept-Encoding" ":"
+	 * 	                   1#( codings [ ";" "q" "=" qvalue ] )
+	 * 	codings          = ( content-coding | "*" )
+	 * </p>
+	 * <p>
+	 * Examples of its use are:
+	 * <p class='bcode'>
+	 * 	Accept-Encoding: compress, gzip
+	 * 	Accept-Encoding:
+	 * 	Accept-Encoding: *
+	 * 	Accept-Encoding: compress;q=0.5, gzip;q=1.0
+	 * 	Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
+	 * </p>
+	 *
+	 * @param value The value to parse.  If <jk>null</jk> or empty, returns a single <code>TypeRange</code> is returned that represents all types.
+	 * @return The type ranges described by the string.
+	 * <br>The ranges are sorted such that the most acceptable type is available at ordinal position <js>'0'</js>, and the least acceptable at position n-1.
+	 */
+	public static TypeRange[] parse(String value) {
+
+		if (value == null || value.length() == 0)
+			return DEFAULT;
+
+		if (value.indexOf(',') == -1)
+			return new TypeRange[]{new TypeRange(value)};
+
+		Set<TypeRange> ranges = new TreeSet<TypeRange>();
+
+		for (String r : StringUtils.split(value, ',')) {
+			r = r.trim();
+
+			if (r.isEmpty())
+				continue;
+
+			ranges.add(new TypeRange(r));
+		}
+
+		return ranges.toArray(new TypeRange[ranges.size()]);
+	}
+
+	@SuppressWarnings("unchecked")
+	private TypeRange(String token) {
+		Builder b = new Builder(token);
+		this.type = b.type;
+		this.qValue = b.qValue;
+		this.extensions = (b.extensions == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.extensions));
+	}
+
+	private static class Builder {
+		private String type;
+		private Float qValue = 1f;
+		private Map<String,Set<String>> extensions;
+
+		private Builder(String token) {
+
+			token = token.trim();
+
+			int i = token.indexOf(";q=");
+
+			if (i == -1) {
+				type = token;
+				return;
+			}
+
+			type = token.substring(0, i);
+
+			String[] tokens = token.substring(i+1).split(";");
+
+			// Only the type of the range is specified
+			if (tokens.length > 0) {
+				boolean isInExtensions = false;
+				for (int j = 0; j < tokens.length; j++) {
+					String[] parm = tokens[j].split("=");
+					if (parm.length == 2) {
+						String k = parm[0], v = parm[1];
+						if (isInExtensions) {
+							if (extensions == null)
+								extensions = new TreeMap<String,Set<String>>();
+							if (! extensions.containsKey(k))
+								extensions.put(k, new TreeSet<String>());
+							extensions.get(k).add(v);
+						} else if (k.equals("q")) {
+							qValue = new Float(v);
+							isInExtensions = true;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Returns the type enclosed by this type range.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <ul>
+	 * 	<li><js>"compress"</js>
+	 * 	<li><js>"gzip"</js>
+	 * 	<li><js>"*"</js>
+	 * </ul>
+	 *
+	 * @return The type of this type range, lowercased, never <jk>null</jk>.
+	 */
+	public String getType() {
+		return type;
+	}
+
+	/**
+	 * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616.
+	 * <p>
+	 * The quality value is a float between <code>0.0</code> (unacceptable) and <code>1.0</code> (most acceptable).
+	 * <p>
+	 * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js> header, as opposed to <js>"accept-*"</js>
+	 * header, its value will always be <js>"1"</js>.
+	 *
+	 * @return The 'q' value for this type, never <jk>null</jk>.
+	 */
+	public Float getQValue() {
+		return qValue;
+	}
+
+	/**
+	 * Returns the optional set of custom extensions defined for this type.
+	 * <p>
+	 * Values are lowercase and never <jk>null</jk>.
+	 *
+	 * @return The optional list of extensions, never <jk>null</jk>.
+	 */
+	public Map<String,Set<String>> getExtensions() {
+		return extensions;
+	}
+
+	/**
+	 * Provides a string representation of this media range, suitable for use as an <code>Accept</code> header value.
+	 * <p>
+	 * The literal text generated will be all lowercase.
+	 *
+	 * @return A media range suitable for use as an Accept header value, never <code>null</code>.
+	 */
+	@Override /* Object */
+	public String toString() {
+		StringBuffer sb = new StringBuffer().append(type);
+
+		// '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue.
+		if (qValue.floatValue() == 1.0) {
+			if (! extensions.isEmpty()) {
+				sb.append(";q=").append(qValue);
+				for (Entry<String,Set<String>> e : extensions.entrySet()) {
+					String k = e.getKey();
+					for (String v : e.getValue())
+						sb.append(';').append(k).append('=').append(v);
+				}
+			}
+		} else {
+			sb.append(";q=").append(qValue);
+			for (Entry<String,Set<String>> e : extensions.entrySet()) {
+				String k = e.getKey();
+				for (String v : e.getValue())
+					sb.append(';').append(k).append('=').append(v);
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the specified object is also a <code>MediaType</code>, and has the same qValue, type, parameters, and extensions.
+	 *
+	 * @return <jk>true</jk> if object is equivalent.
+	 */
+	@Override /* Object */
+	public boolean equals(Object o) {
+
+		if (o == null || !(o instanceof TypeRange))
+			return false;
+
+		if (this == o)
+			return true;
+
+		TypeRange o2 = (TypeRange) o;
+		return qValue.equals(o2.qValue)
+			&& type.equals(o2.type)
+			&& extensions.equals(o2.extensions);
+	}
+
+	/**
+	 * Returns a hash based on this instance's <code>media-type</code>.
+	 *
+	 * @return A hash based on this instance's <code>media-type</code>.
+	 */
+	@Override /* Object */
+	public int hashCode() {
+		return type.hashCode();
+	}
+
+	/**
+	 * Compares two MediaRanges for equality.
+	 * <p>
+	 * The values are first compared according to <code>qValue</code> values.
+	 * Should those values be equal, the <code>type</code> is then lexicographically compared (case-insensitive) in ascending order,
+	 * 	with the <js>"*"</js> type demoted last in that order.
+	 * <code>TypeRanges</code> with the same types but with extensions are promoted over those same types with no extensions.
+	 *
+	 * @param o The range to compare to.  Never <jk>null</jk>.
+	 */
+	@Override /* Comparable */
+	public int compareTo(TypeRange o) {
+
+		// Compare q-values.
+		int qCompare = Float.compare(o.qValue, qValue);
+		if (qCompare != 0)
+			return qCompare;
+
+		// Compare media-types.
+		// Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison.
+		int i = o.type.toString().compareTo(type.toString());
+		return i;
+	}
+
+	/**
+	 * Checks if the specified type matches this range.
+	 * <p>
+	 * The type will match this range if the range type string is the same or <js>"*"</js>.
+	 *
+	 * @param type The type to match against this range.
+	 * @return <jk>true</jk> if the specified type matches this range.
+	 */
+	@SuppressWarnings("hiding")
+	public boolean matches(String type) {
+		if (qValue == 0)
+			return false;
+		return this.type.equals(type) || this.type.equals("*");
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
index 5bc7009..b8c504c 100644
--- a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
+++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
@@ -15,6 +15,7 @@ package org.apache.juneau.ini;
 import java.io.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 
 /**
  * Wraps a {@link ConfigFile} in a {@link Writable} to be rendered as plain text.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/internal/ArrayUtils.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/ArrayUtils.java b/juneau-core/src/main/java/org/apache/juneau/internal/ArrayUtils.java
index effd363..5199af1 100644
--- a/juneau-core/src/main/java/org/apache/juneau/internal/ArrayUtils.java
+++ b/juneau-core/src/main/java/org/apache/juneau/internal/ArrayUtils.java
@@ -271,6 +271,39 @@ public final class ArrayUtils {
 	}
 
 	/**
+	 * Returns <jk>true</jk> if the specified array contains the specified element
+	 * 	using the {@link String#equals(Object)} method.
+	 *
+	 * @param element The element to check for.
+	 * @param array The array to check.
+	 * @return <jk>true</jk> if the specified array contains the specified element,
+	 * 	<jk>false</jk> if the array or element is <jk>null</jk>.
+	 */
+	public static boolean contains(String element, String[] array) {
+		return indexOf(element, array) != -1;
+	}
+
+	/**
+	 * Returns the index position of the element in the specified array
+	 * 	using the {@link String#equals(Object)} method.
+	 *
+	 * @param element The element to check for.
+	 * @param array The array to check.
+	 * @return The index position of the element in the specified array, or
+	 * 	<code>-1</code> if the array doesn't contain the element, or the array or element is <jk>null</jk>.
+	 */
+	public static int indexOf(String element, String[] array) {
+		if (element == null)
+			return -1;
+		if (array == null)
+			return -1;
+		for (int i = 0; i < array.length; i++)
+			if (element.equals(array[i]))
+				return i;
+		return -1;
+	}
+
+	/**
 	 * Converts a primitive wrapper array (e.g. <code>Integer[]</code>) to a primitive array (e.g. <code><jk>int</jk>[]</code>).
 	 *
 	 * @param o The array to convert.  Must be a primitive wrapper array.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/internal/CollectionUtils.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/CollectionUtils.java b/juneau-core/src/main/java/org/apache/juneau/internal/CollectionUtils.java
index c413427..6c12246 100644
--- a/juneau-core/src/main/java/org/apache/juneau/internal/CollectionUtils.java
+++ b/juneau-core/src/main/java/org/apache/juneau/internal/CollectionUtils.java
@@ -52,4 +52,36 @@ public class CollectionUtils {
 			l.add(o);
 		return l;
 	}
+
+	/**
+	 * Adds the contents of one list to the other in reverse order.
+	 * <p>
+	 * i.e. add values from 2nd list from end-to-start order to the end of the 1st list.
+	 *
+	 * @param list The list to append to.
+	 * @param append Contains the values to append to the list.
+	 * @return The same list.
+	 */
+	@SuppressWarnings({ "rawtypes", "unchecked" })
+	public static List<?> addReverse(List list, List append) {
+		for (ListIterator i = append.listIterator(append.size()); i.hasPrevious();)
+			list.add(i.previous());
+		return list;
+	}
+
+	/**
+	 * Adds the contents of the array to the list in reverse order.
+	 * <p>
+	 * i.e. add values from the array from end-to-start order to the end of the list.
+	 *
+	 * @param list The list to append to.
+	 * @param append Contains the values to append to the list.
+	 * @return The same list.
+	 */
+	@SuppressWarnings({ "rawtypes", "unchecked" })
+	public static List<?> addReverse(List list, Object[] append) {
+		for (int i = append.length - 1; i >= 0; i--)
+			list.add(append[i]);
+		return list;
+	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java b/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java
index a5bc00e..bee95d9 100644
--- a/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java
+++ b/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java
@@ -480,6 +480,8 @@ public final class StringUtils {
 			return null;
 		if (isEmpty(s))
 			return new String[0];
+		if (s.indexOf(c) == -1)
+			return new String[]{s};
 
 		List<String> l = new LinkedList<String>();
 		char[] sArray = s.toCharArray();
@@ -522,6 +524,83 @@ public final class StringUtils {
 	}
 
 	/**
+	 * Splits a list of key-value pairs into an ordered map.
+	 * <p>
+	 * Example:
+	 * <p class='bcode'>
+	 * 	String in = <js>"foo=1;bar=2"</js>;
+	 * 	Map m = StringUtils.<jsm>splitMap</jsm>(in, <js>';'</js>, <js>'='</js>, <jk>true</jk>);
+	 * </p>
+	 *
+	 * @param s The string to split.
+	 * @param delim The delimiter between the key-value pairs.
+	 * @param eq The delimiter between the key and value.
+	 * @param trim Trim strings after parsing.
+	 * @return The parsed map.  Never <jk>null</jk>.
+	 */
+	@SuppressWarnings("unchecked")
+	public static Map<String,String> splitMap(String s, char delim, char eq, boolean trim) {
+
+		char[] unEscapeChars = new char[]{'\\', delim, eq};
+
+		if (s == null)
+			return null;
+		if (isEmpty(s))
+			return Collections.EMPTY_MAP;
+
+		Map<String,String> m = new LinkedHashMap<String,String>();
+
+		int
+			S1 = 1,  // Found start of key, looking for equals.
+			S2 = 2;  // Found equals, looking for delimiter (or end).
+
+		int state = S1;
+
+		char[] sArray = s.toCharArray();
+		int x1 = 0, escapeCount = 0;
+		String key = null;
+		for (int i = 0; i < sArray.length + 1; i++) {
+			char c = i == sArray.length ? delim : sArray[i];
+			if (c == '\\')
+				escapeCount++;
+			if (escapeCount % 2 == 0) {
+				if (state == S1) {
+					if (c == eq) {
+						key = s.substring(x1, i);
+						if (trim)
+							key = trim(key);
+						key = unEscapeChars(key, unEscapeChars);
+						state = S2;
+						x1 = i+1;
+					} else if (c == delim) {
+						key = s.substring(x1, i);
+						if (trim)
+							key = trim(key);
+						key = unEscapeChars(key, unEscapeChars);
+						m.put(key, "");
+						state = S1;
+						x1 = i+1;
+					}
+				} else if (state == S2) {
+					if (c == delim) {
+						String val = s.substring(x1, i);
+						if (trim)
+							val = trim(val);
+						val = unEscapeChars(val, unEscapeChars);
+						m.put(key, val);
+						key = null;
+						x1 = i+1;
+						state = S1;
+					}
+				}
+			}
+			if (c != '\\') escapeCount = 0;
+		}
+
+		return m;
+	}
+
+	/**
 	 * Returns <jk>true</jk> if specified string is <jk>null</jk> or empty.
 	 *
 	 * @param s The string to check.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/jso/JsoParserBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/jso/JsoParserBuilder.java b/juneau-core/src/main/java/org/apache/juneau/jso/JsoParserBuilder.java
index 2608028..210460b 100644
--- a/juneau-core/src/main/java/org/apache/juneau/jso/JsoParserBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/jso/JsoParserBuilder.java
@@ -15,6 +15,7 @@ package org.apache.juneau.jso;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.parser.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/jso/JsoSerializerBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/jso/JsoSerializerBuilder.java b/juneau-core/src/main/java/org/apache/juneau/jso/JsoSerializerBuilder.java
index c4d7d40..88d89c5 100644
--- a/juneau-core/src/main/java/org/apache/juneau/jso/JsoSerializerBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/jso/JsoSerializerBuilder.java
@@ -15,6 +15,7 @@ package org.apache.juneau.jso;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java
index d362112..c07c8b7 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java
@@ -20,6 +20,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.transform.*;

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonParserBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonParserBuilder.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonParserBuilder.java
index ed550cc..1d6a3ad 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonParserBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonParserBuilder.java
@@ -15,6 +15,7 @@ package org.apache.juneau.json;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.parser.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonParserSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonParserSession.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonParserSession.java
index 02ae190..f495bca 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonParserSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonParserSession.java
@@ -17,6 +17,7 @@ import java.lang.reflect.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.parser.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializer.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializer.java
index 3e28eab..df974ea 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializer.java
@@ -20,6 +20,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.transform.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializerBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializerBuilder.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializerBuilder.java
index 9a8616f..eb4a1ea 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializerBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonSchemaSerializerBuilder.java
@@ -15,6 +15,7 @@ package org.apache.juneau.json;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 
 /**
  * Builder class for building instances of JSON Schema serializers.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializer.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializer.java
index 91945d4..71859f4 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializer.java
@@ -19,6 +19,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.transform.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerBuilder.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerBuilder.java
index 97323aa..0715dd1 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerBuilder.java
@@ -17,6 +17,7 @@ import static org.apache.juneau.json.JsonSerializerContext.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
index dd8b8a1..ef1a975 100644
--- a/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonSerializerSession.java
@@ -18,6 +18,7 @@ import java.lang.reflect.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
index 8895f33..bd9128c 100644
--- a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
+++ b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
@@ -19,6 +19,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.transform.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserBuilder.java b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserBuilder.java
index 3e68801..26df53b 100644
--- a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserBuilder.java
@@ -15,6 +15,7 @@ package org.apache.juneau.msgpack;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.parser.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
index 24a40e7..57beda3 100644
--- a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackParserSession.java
@@ -17,6 +17,7 @@ import java.lang.reflect.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.parser.*;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
index 89e736b..e0108ea 100644
--- a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
@@ -17,6 +17,7 @@ import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.transform.*;
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/c3609d05/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerBuilder.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerBuilder.java b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerBuilder.java
index f909e14..2c29538 100644
--- a/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerBuilder.java
+++ b/juneau-core/src/main/java/org/apache/juneau/msgpack/MsgPackSerializerBuilder.java
@@ -15,6 +15,7 @@ package org.apache.juneau.msgpack;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.serializer.*;
 
 /**



Mime
View raw message