You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2021/04/04 12:55:37 UTC

[juneau] branch master updated: Performance improvements around HeaderList and PartList.

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 9550315  Performance improvements around HeaderList and PartList.
9550315 is described below

commit 9550315cd477fa4b926327f97479e936c83fe474
Author: JamesBognar <ja...@salesforce.com>
AuthorDate: Sun Apr 4 08:55:15 2021 -0400

    Performance improvements around HeaderList and PartList.
---
 .../juneau/http/header/BasicHeaderIterator.java    | 104 ++++
 .../apache/juneau/http/header/HeaderBeanMeta.java  |   8 +-
 .../org/apache/juneau/http/header/HeaderList.java  | 113 ++--
 .../juneau/http/header/HeaderListBuilder.java      | 212 ++++++--
 .../apache/juneau/http/part/BasicPartIterator.java |  97 ++++
 .../HeaderBeanMeta.java => part/PartBeanMeta.java} |  85 +--
 .../org/apache/juneau/http/part/PartIterator.java  |  68 ++-
 .../java/org/apache/juneau/http/part/PartList.java | 537 ++++++++++++++++---
 .../apache/juneau/http/part/PartListBuilder.java   | 490 ++++++++++++-----
 .../org/apache/juneau/internal/ArrayBuilder.java   |  12 +-
 .../org/apache/juneau/internal/StringUtils.java    |  12 +
 .../org/apache/juneau/rest/client/RestClient.java  |  18 +-
 .../org/apache/juneau/rest/client/RestRequest.java |  51 +-
 .../apache/juneau/rest/RestOperationContext.java   |   8 +-
 .../java/org/apache/juneau/http/PartList_Test.java | 101 ----
 .../org/apache/juneau/http/header/Accept_Test.java |   3 +
 .../juneau/http/header/HeaderBeanMeta_Test.java    |   2 +-
 .../apache/juneau/http/header/HeaderList_Test.java |  97 ++++
 .../PartBeanMeta_Test.java}                        |  38 +-
 .../org/apache/juneau/http/part/PartList_Test.java | 580 +++++++++++++++++++++
 .../remote/Remote_FormDataAnnotation_Test.java     |  12 +-
 .../http/remote/Remote_PathAnnotation_Test.java    |  16 +-
 .../http/remote/Remote_QueryAnnotation_Test.java   |  15 +-
 .../rest/client/RestClient_FormData_Test.java      |   1 -
 .../org/apache/juneau/utils/ArrayBuilder_Test.java |   2 +-
 25 files changed, 2124 insertions(+), 558 deletions(-)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/BasicHeaderIterator.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/BasicHeaderIterator.java
new file mode 100644
index 0000000..4aa0ffe
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/BasicHeaderIterator.java
@@ -0,0 +1,104 @@
+// ***************************************************************************************************************************
+// * 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.header;
+
+import static org.apache.juneau.assertions.Assertions.*;
+
+import java.util.NoSuchElementException;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderIterator;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Basic implementation of a {@link HeaderIterator}.
+ *
+ * {@review}
+ */
+@NotThreadSafe
+public class BasicHeaderIterator implements HeaderIterator {
+
+	private final Header[] entries;
+	private final String name;
+	private final boolean caseSensitive;
+
+	private int currentIndex;
+
+	/**
+	 * Creates a new header iterator.
+	 *
+	 * @param headers An array of headers over which to iterate.
+	 * @param name The name of the headers over which to iterate, or <jk>null</jk> for all.
+	 * @param caseSensitive Use case-sensitive matching for part name.
+	 */
+	public BasicHeaderIterator(Header[] headers, String name, boolean caseSensitive) {
+		this.entries = assertArgNotNull("headers", headers);
+		this.name = name;
+		this.caseSensitive = caseSensitive;
+		this.currentIndex = findNext(-1);
+	}
+
+	private int findNext(int pos) {
+
+		int from = pos;
+
+		int to = entries.length - 1;
+		boolean found = false;
+		while (!found && (from < to)) {
+			from++;
+			found = filter(from);
+		}
+
+		return found ? from : -1;
+	}
+
+	private boolean filter(int index) {
+		return (name == null) || eq(name, entries[index].getName());
+	}
+
+	@Override /* HeaderIterator */
+	public boolean hasNext() {
+		return (currentIndex >= 0);
+	}
+
+	@Override /* HeaderIterator */
+	public Header nextHeader() throws NoSuchElementException {
+
+		int current = currentIndex;
+
+		if (current < 0)
+			throw new NoSuchElementException("Iteration already finished.");
+
+		currentIndex = findNext(current);
+
+		return entries[current];
+	}
+
+	@Override /* HeaderIterator */
+	public final Object next() throws NoSuchElementException {
+		return nextHeader();
+	}
+
+	/**
+	 * Not supported.
+	 */
+	@Override /* HeaderIterator */
+	public void remove() {
+		throw new UnsupportedOperationException("Remove is not supported.");
+	}
+
+	private boolean eq(String s1, String s2) {
+		return StringUtils.eq(!caseSensitive, s1, s2);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java
index a3978f3..7e7148b 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java
@@ -31,10 +31,10 @@ import org.apache.juneau.reflect.*;
  * <p>
  * Header beans must have one of the following public constructors:
  * <ul>
- * 	<li><c>X(String <jv>headerValue</jv>)</c>
- * 	<li><c>X(Object <jv>headerValue</jv>)</c>
- * 	<li><c>X(String <jv>headerName</jv>, String <jv>headerValue</jv>)</c>
- * 	<li><c>X(String <jv>headerName</jv>, Object <jv>headerValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(String <jv>headerValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(Object <jv>headerValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(String <jv>headerName</jv>, String <jv>headerValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(String <jv>headerName</jv>, Object <jv>headerValue</jv>)</c>
  * </ul>
  *
  * <p>
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderList.java
index 207151e..51b4194 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderList.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderList.java
@@ -20,7 +20,6 @@ import java.util.function.*;
 import java.util.stream.*;
 
 import org.apache.http.*;
-import org.apache.http.message.*;
 import org.apache.http.util.*;
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
@@ -65,6 +64,28 @@ import org.apache.juneau.svl.*;
  * </p>
  *
  * <p>
+ * The builder class supports setting default header values (i.e. add a header to the list if it isn't otherwise in the list).
+ * Note that this is different from simply setting a value twice as using default values will not overwrite existing
+ * headers.
+ * <br>The following example notes the distinction:
+ *
+ * <p class='bcode w800'>
+ * 	<jv>headers</jv> = HeaderList
+ * 		.<jsm>create</jsm>()
+ * 		.set(Accept.<jsf>TEXT_PLAIN</jsf>)
+ * 		.set(Accept.<jsf>TEXT_XML</jsf>)
+ * 		.build();
+ * 	<jsm>assertObject</jsm>(<jv>headers</jv>).isString(<js>"[Accept: text/xml]"</js>);
+ *
+ * 	<jv>headers</jv> = HeaderList
+ * 		.create()
+ * 		.set(Accept.<jsf>TEXT_PLAIN</jsf>)
+ * 		.setDefault(Accept.<jsf>TEXT_XML</jsf>)
+ * 		.build();
+ * 	<jsm>assertObject</jsm>(<jv>headers</jv>).isString(<js>"[Accept: text/plain]"</js>);
+ * </p>
+ *
+ * <p>
  * Various methods are provided for iterating over the headers in this list to avoid array copies.
  * <ul class='javatree'>
  * 	<li class='jm'>{@link #forEach(Consumer)} / {@link #forEach(String,Consumer)} - Use consumers to process headers.
@@ -148,7 +169,7 @@ public class HeaderList {
 	/** Predefined instance. */
 	public static final HeaderList EMPTY = new HeaderList();
 
-	final Header[] headers;
+	final Header[] entries;
 	final boolean caseSensitive;
 
 	/**
@@ -204,7 +225,7 @@ public class HeaderList {
 			return EMPTY;
 		if (pairs.length % 2 != 0)
 			throw new BasicRuntimeException("Odd number of parameters passed into HeaderList.ofPairs()");
-		ArrayBuilder<Header> b = ArrayBuilder.create(Header.class, pairs.length / 2);
+		ArrayBuilder<Header> b = ArrayBuilder.create(Header.class, pairs.length / 2, true);
 		for (int i = 0; i < pairs.length; i+=2)
 			b.add(BasicHeader.of(stringify(pairs[i]), pairs[i+1]));
 		return new HeaderList(b.toArray());
@@ -216,7 +237,25 @@ public class HeaderList {
 	 * @param builder The builder containing the settings for this bean.
 	 */
 	public HeaderList(HeaderListBuilder builder) {
-		this.headers = builder.headers.toArray(new Header[builder.headers.size()]);
+		if (builder.defaultEntries == null) {
+			entries = builder.entries.toArray(new Header[builder.entries.size()]);
+		} else {
+			ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, builder.entries.size() + builder.defaultEntries.size(), true);
+
+			for (int i = 0, j = builder.entries.size(); i < j; i++)
+				l.add(builder.entries.get(i));
+
+			for (int i1 = 0, j1 = builder.defaultEntries.size(); i1 < j1; i1++) {
+				Header x = builder.defaultEntries.get(i1);
+				boolean exists = false;
+				for (int i2 = 0, j2 = builder.entries.size(); i2 < j2 && ! exists; i2++)
+					exists = eq(builder.entries.get(i2).getName(), x.getName());
+				if (! exists)
+					l.add(x);
+			}
+
+			entries = l.toArray();
+		}
 		this.caseSensitive = builder.caseSensitive;
 	}
 
@@ -229,10 +268,10 @@ public class HeaderList {
 	 * 	<br><jk>null</jk> entries are ignored.
 	 */
 	protected HeaderList(List<Header> headers) {
-		ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, headers.size());
+		ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, headers.size(), true);
 		for (int i = 0, j = headers.size(); i < j; i++)
 			l.add(headers.get(i));
-		this.headers = l.toArray();
+		entries = l.toArray();
 		caseSensitive = false;
 	}
 
@@ -244,10 +283,10 @@ public class HeaderList {
 	 * 	<br><jk>null</jk> entries are ignored.
 	 */
 	protected HeaderList(Header...headers) {
-		ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, headers.length);
+		ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, headers.length, true);
 		for (int i = 0; i < headers.length; i++)
 			l.add(headers[i]);
-		this.headers = l.toArray();
+		entries = l.toArray();
 		caseSensitive = false;
 	}
 
@@ -255,7 +294,7 @@ public class HeaderList {
 	 * Default constructor.
 	 */
 	protected HeaderList() {
-		this.headers = EMPTY_ARRAY;
+		entries = EMPTY_ARRAY;
 		caseSensitive = false;
 	}
 
@@ -282,8 +321,8 @@ public class HeaderList {
 
 		Header first = null;
 		List<Header> rest = null;
-		for (int i = 0; i < headers.length; i++) {
-			Header x = headers[i];
+		for (int i = 0; i < entries.length; i++) {
+			Header x = entries[i];
 			if (eq(x.getName(), name)) {
 				if (first == null)
 					first = x;
@@ -341,8 +380,8 @@ public class HeaderList {
 
 		Header first = null;
 		List<Header> rest = null;
-		for (int i = 0; i < headers.length; i++) {
-			Header x = headers[i];
+		for (int i = 0; i < entries.length; i++) {
+			Header x = entries[i];
 			if (eq(x.getName(), name)) {
 				if (first == null)
 					first = x;
@@ -388,11 +427,11 @@ public class HeaderList {
 	public <T> Optional<T> get(Class<T> type) {
 		assertArgNotNull("type", type);
 
-		String headerName = HeaderBeanMeta.of(type).getSchema().getName();
-		if (headerName == null)
+		String name = HeaderBeanMeta.of(type).getSchema().getName();
+		if (name == null)
 			throw new BasicIllegalArgumentException("Header name could not be found on bean type ''{0}''", type.getName());
 
-		return get(headerName, type);
+		return get(name, type);
 	}
 
 	/**
@@ -410,8 +449,8 @@ public class HeaderList {
 	 */
 	public Header[] getAll(String name) {
 		List<Header> l = null;
-		for (int i = 0; i < headers.length; i++) {
-			Header x = headers[i];
+		for (int i = 0; i < entries.length; i++) {
+			Header x = entries[i];
 			if (eq(x.getName(), name)) {
 				if (l == null)
 					l = new ArrayList<>();
@@ -427,7 +466,7 @@ public class HeaderList {
 	 * @return The number of headers in this list.
 	 */
 	public int size() {
-		return headers.length;
+		return entries.length;
 	}
 
 	/**
@@ -440,8 +479,8 @@ public class HeaderList {
 	 * @return The first matching header, or <jk>null</jk> if not found.
 	 */
 	public Optional<Header> getFirst(String name) {
-		for (int i = 0; i < headers.length; i++) {
-			Header x = headers[i];
+		for (int i = 0; i < entries.length; i++) {
+			Header x = entries[i];
 			if (eq(x.getName(), name))
 				return Optional.of(x);
 		}
@@ -458,8 +497,8 @@ public class HeaderList {
 	 * @return The last matching header, or <jk>null</jk> if not found.
 	 */
 	public Optional<Header> getLast(String name) {
-		for (int i = headers.length - 1; i >= 0; i--) {
-			Header x = headers[i];
+		for (int i = entries.length - 1; i >= 0; i--) {
+			Header x = entries[i];
 			if (eq(x.getName(), name))
 				return Optional.of(x);
 		}
@@ -472,7 +511,7 @@ public class HeaderList {
 	 * @return An array of all the headers in this list, or an empty array if no headers are present.
 	 */
 	public Header[] getAll() {
-		return headers.length == 0 ? EMPTY_ARRAY : Arrays.copyOf(headers, headers.length);
+		return entries.length == 0 ? EMPTY_ARRAY : Arrays.copyOf(entries, entries.length);
 	}
 
 	/**
@@ -485,8 +524,8 @@ public class HeaderList {
 	 * @return <jk>true</jk> if at least one header with the name is present.
 	 */
 	public boolean contains(String name) {
-		for (int i = 0; i < headers.length; i++) {
-			Header x = headers[i];
+		for (int i = 0; i < entries.length; i++) {
+			Header x = entries[i];
 			if (eq(x.getName(), name))
 				return true;
 		}
@@ -499,7 +538,7 @@ public class HeaderList {
 	 * @return A new iterator over this list of headers.
 	 */
 	public HeaderIterator iterator() {
-		return new BasicHeaderIterator(headers, null);
+		return new BasicHeaderIterator(entries, null, caseSensitive);
 	}
 
 	/**
@@ -510,7 +549,7 @@ public class HeaderList {
 	 * @return A new iterator over the matching headers in this list.
 	 */
 	public HeaderIterator iterator(String name) {
-		return new BasicHeaderIterator(headers, name);
+		return new BasicHeaderIterator(entries, name, caseSensitive);
 	}
 
 	/**
@@ -524,8 +563,8 @@ public class HeaderList {
 	 * @return This object (for method chaining).
 	 */
 	public HeaderList forEach(Consumer<Header> c) {
-		for (int i = 0; i < headers.length; i++)
-			c.accept(headers[i]);
+		for (int i = 0; i < entries.length; i++)
+			c.accept(entries[i]);
 		return this;
 	}
 
@@ -541,9 +580,9 @@ public class HeaderList {
 	 * @return This object (for method chaining).
 	 */
 	public HeaderList forEach(String name, Consumer<Header> c) {
-		for (int i = 0; i < headers.length; i++)
-			if (eq(name, headers[i].getName()))
-				c.accept(headers[i]);
+		for (int i = 0; i < entries.length; i++)
+			if (eq(name, entries[i].getName()))
+				c.accept(entries[i]);
 		return this;
 	}
 
@@ -556,7 +595,7 @@ public class HeaderList {
 	 * @return This object (for method chaining).
 	 */
 	public Stream<Header> stream() {
-		return Arrays.stream(headers);
+		return Arrays.stream(entries);
 	}
 
 	/**
@@ -569,15 +608,15 @@ public class HeaderList {
 	 * @return This object (for method chaining).
 	 */
 	public Stream<Header> stream(String name) {
-		return Arrays.stream(headers).filter(x->eq(name, x.getName()));
+		return Arrays.stream(entries).filter(x->eq(name, x.getName()));
 	}
 
 	private boolean eq(String s1, String s2) {
-		return caseSensitive ? StringUtils.eq(s1, s2) : StringUtils.eqic(s1, s2);
+		return StringUtils.eq(!caseSensitive, s1, s2);
 	}
 
 	@Override /* Object */
 	public String toString() {
-		return Arrays.asList(headers).toString();
+		return Arrays.asList(entries).toString();
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderListBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderListBuilder.java
index 6ce7ad3..af8390c 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderListBuilder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderListBuilder.java
@@ -30,9 +30,10 @@ import org.apache.juneau.svl.*;
 @NotThreadSafe
 public class HeaderListBuilder {
 
-	final List<Header> headers;
+	final List<Header> entries;
+	List<Header> defaultEntries;
 	private VarResolver varResolver;
-	boolean caseSensitive = false;
+	boolean caseSensitive;
 
 	/** Predefined instance. */
 	private static final HeaderList EMPTY = new HeaderList();
@@ -41,7 +42,7 @@ public class HeaderListBuilder {
 	 * Constructor.
 	 */
 	public HeaderListBuilder() {
-		headers = new ArrayList<>();
+		entries = new ArrayList<>();
 	}
 
 	/**
@@ -50,9 +51,9 @@ public class HeaderListBuilder {
 	 * @param copyFrom The bean to copy from.
 	 */
 	public HeaderListBuilder(HeaderList copyFrom) {
-		headers = new ArrayList<>(copyFrom.headers.length);
-		for (int i = 0; i < copyFrom.headers.length; i++)
-			headers.add(copyFrom.headers[i]);
+		entries = new ArrayList<>(copyFrom.entries.length);
+		for (int i = 0; i < copyFrom.entries.length; i++)
+			entries.add(copyFrom.entries[i]);
 		caseSensitive = copyFrom.caseSensitive;
 	}
 
@@ -62,7 +63,7 @@ public class HeaderListBuilder {
 	 * @return A new {@link HeaderList} bean.
 	 */
 	public HeaderList build() {
-		return headers.isEmpty() ? EMPTY : new HeaderList(this);
+		return entries.isEmpty() && defaultEntries == null ? EMPTY : new HeaderList(this);
 	}
 
 	/**
@@ -77,6 +78,8 @@ public class HeaderListBuilder {
 	 * 	<li class='jm'>{@link #prepend(String, Supplier) prepend(String,Supplier&lt;?&gt;)}
 	 * 	<li class='jm'>{@link #set(String, Object) set(String,Object)}
 	 * 	<li class='jm'>{@link #set(String, Supplier) set(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #setDefault(String, Object) set(String,Object)}
+	 * 	<li class='jm'>{@link #setDefault(String, Supplier) set(String,Supplier&lt;?&gt;)}
 	 * </ul>
 	 *
 	 * <p>
@@ -100,6 +103,8 @@ public class HeaderListBuilder {
 	 * 	<li class='jm'>{@link #prepend(String, Supplier) prepend(String,Supplier&lt;?&gt;)}
 	 * 	<li class='jm'>{@link #set(String, Object) set(String,Object)}
 	 * 	<li class='jm'>{@link #set(String, Supplier) set(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #setDefault(String, Object) set(String,Object)}
+	 * 	<li class='jm'>{@link #setDefault(String, Supplier) set(String,Supplier&lt;?&gt;)}
 	 * </ul>
 	 *
 	 * @param varResolver The variable resolver to use for resolving variables.
@@ -119,7 +124,7 @@ public class HeaderListBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public HeaderListBuilder caseSensitive() {
-		this.caseSensitive = true;
+		caseSensitive = true;
 		return this;
 	}
 
@@ -130,7 +135,7 @@ public class HeaderListBuilder {
 	 */
 	@FluentSetter
 	public HeaderListBuilder clear() {
-		headers.clear();
+		entries.clear();
 		return this;
 	}
 
@@ -143,8 +148,8 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder append(HeaderList value) {
 		if (value != null)
-			for (Header h : value.headers)
-				append(h);
+			for (Header x : value.entries)
+				append(x);
 		return this;
 	}
 
@@ -157,7 +162,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder append(Header value) {
 		if (value != null)
-			headers.add(value);
+			entries.add(value);
 		return this;
 	}
 
@@ -201,7 +206,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder append(Header...values) {
 		if (values != null)
-			for (int i = 0; i < values.length; i++) /* See HTTPCORE-361 */
+			for (int i = 0; i < values.length; i++)
 				append(values[i]);
 		return this;
 	}
@@ -229,7 +234,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder prepend(HeaderList value) {
 		if (value != null)
-			headers.addAll(0, Arrays.asList(value.headers));
+			entries.addAll(0, Arrays.asList(value.entries));
 		return this;
 	}
 
@@ -242,7 +247,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder prepend(Header value) {
 		if (value != null)
-			headers.add(0, value);
+			entries.add(0, value);
 		return this;
 	}
 
@@ -299,7 +304,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder prepend(List<Header> values) {
 		if (values != null)
-			headers.addAll(0, values);
+			entries.addAll(0, values);
 		return this;
 	}
 
@@ -312,8 +317,8 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder remove(HeaderList value) {
 		if (value != null)
-			for (int i = 0; i < value.headers.length; i++) /* See HTTPCORE-361 */
-				remove(value.headers[i]);
+			for (int i = 0; i < value.entries.length; i++)
+				remove(value.entries[i]);
 		return this;
 	}
 
@@ -326,7 +331,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder remove(Header value) {
 		if (value != null)
-			headers.remove(value);
+			entries.remove(value);
 		return this;
 	}
 
@@ -338,7 +343,7 @@ public class HeaderListBuilder {
 	 */
 	@FluentSetter
 	public HeaderListBuilder remove(Header...values) {
-		for (int i = 0; i < values.length; i++) /* See HTTPCORE-361 */
+		for (int i = 0; i < values.length; i++)
 			remove(values[i]);
 		return this;
 	}
@@ -364,9 +369,9 @@ public class HeaderListBuilder {
 	 */
 	@FluentSetter
 	public HeaderListBuilder remove(String name) {
-		for (int i = 0; i < headers.size(); i++) /* See HTTPCORE-361 */
-			if (eq(headers.get(i).getName(), name))
-				headers.remove(i--);
+		for (int i = 0; i < entries.size(); i++) /* See HTTPCORE-361 */
+			if (eq(entries.get(i).getName(), name))
+				entries.remove(i--);
 		return this;
 	}
 
@@ -379,7 +384,7 @@ public class HeaderListBuilder {
 	@FluentSetter
 	public HeaderListBuilder remove(String...names) {
 		if (names != null)
-			for (int i = 0; i < names.length; i++) /* See HTTPCORE-361 */
+			for (int i = 0; i < names.length; i++)
 				remove(names[i]);
 		return this;
 	}
@@ -397,21 +402,21 @@ public class HeaderListBuilder {
 	public HeaderListBuilder set(Header value) {
 		if (value != null) {
 			boolean replaced = false;
-			for (int i = 0, j = headers.size(); i < j; i++) {
-				Header x = headers.get(i);
+			for (int i = 0, j = entries.size(); i < j; i++) {
+				Header x = entries.get(i);
 				if (eq(x.getName(), value.getName())) {
 					if (replaced) {
-						headers.remove(i);
+						entries.remove(i);
 						j--;
 					} else {
-						headers.set(i, value);
+						entries.set(i, value);
 						replaced = true;
 					}
 				}
 			}
 
 			if (! replaced)
-				headers.add(value);
+				entries.add(value);
 		}
 
 		return this;
@@ -468,22 +473,23 @@ public class HeaderListBuilder {
 	public HeaderListBuilder set(List<Header> values) {
 
 		if (values != null) {
-			for (int k = 0, m = values.size(); k < m; k++) {
-				Header h = values.get(k);
+			for (int i1 = 0, j1 = values.size(); i1 < j1; i1++) {
+				Header h = values.get(i1);
 				if (h != null) {
-					for (int i = 0, j = headers.size(); i < j; i++) {
-						Header x = headers.get(i);
+					for (int i2 = 0, j2 = entries.size(); i2 < j2; i2++) {
+						Header x = entries.get(i2);
 						if (eq(x.getName(), h.getName())) {
-							headers.remove(i);
-							j--;
+							entries.remove(i2);
+							j2--;
 						}
 					}
 				}
 			}
 
-			for (Header v : values) {
-				if (v != null) {
-					headers.add(v);
+			for (int i = 0, j = values.size(); i < j; i++) {
+				Header x = values.get(i);
+				if (x != null) {
+					entries.add(x);
 				}
 			}
 		}
@@ -501,7 +507,135 @@ public class HeaderListBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public HeaderListBuilder set(HeaderList values) {
-		return set(values.headers);
+		if (values != null)
+			set(values.entries);
+		return this;
+	}
+
+	/**
+	 * Sets a default value for a header.
+	 *
+	 * <p>
+	 * If no header with the same name is found, the given header is added to the end of the list.
+	 *
+	 * @param value The default header to set.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public HeaderListBuilder setDefault(Header value) {
+		if (value != null) {
+			boolean replaced = false;
+			if (defaultEntries == null)
+				defaultEntries = new ArrayList<>();
+			for (int i = 0, j = defaultEntries.size(); i < j; i++) {
+				Header x = defaultEntries.get(i);
+				if (eq(x.getName(), value.getName())) {
+					if (replaced) {
+						defaultEntries.remove(i);
+						j--;
+					} else {
+						defaultEntries.set(i, value);
+						replaced = true;
+					}
+				}
+			}
+
+			if (! replaced)
+				defaultEntries.add(value);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Sets default values for one or more headers.
+	 *
+	 * <p>
+	 * If no header with the same name is found the given header is added to the end of the list.
+	 *
+	 * @param values The default headers to set.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public HeaderListBuilder setDefault(Header...values) {
+		if (values != null)
+			setDefault(Arrays.asList(values));
+		return this;
+	}
+
+	/**
+	 * Sets a default value for a header.
+	 *
+	 * @param name The header name.
+	 * @param value The header value.
+	 * @return This object (for method chaining).
+	 */
+	public HeaderListBuilder setDefault(String name, Object value) {
+		return setDefault(header(name, value));
+	}
+
+	/**
+	 * Sets a default value for a header.
+	 *
+	 * @param name The header name.
+	 * @param value The header value.
+	 * @return This object (for method chaining).
+	 */
+	public HeaderListBuilder setDefault(String name, Supplier<?> value) {
+		return setDefault(header(name, value));
+	}
+
+	/**
+	 * Sets default values for one or more headers.
+	 *
+	 * <p>
+	 * If no header with the same name is found the given header is added to the end of the list.
+	 *
+	 * @param values The default headers to set.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public HeaderListBuilder setDefault(List<Header> values) {
+
+		if (values != null) {
+			if (defaultEntries == null)
+				defaultEntries = new ArrayList<>();
+			for (int i1 = 0, j1 = values.size(); i1 < j1; i1++) {
+				Header h = values.get(i1);
+				if (h != null) {
+					for (int i2 = 0, j2 = defaultEntries.size(); i2 < j2; i2++) {
+						Header x = defaultEntries.get(i2);
+						if (eq(x.getName(), h.getName())) {
+							defaultEntries.remove(i2);
+							j2--;
+						}
+					}
+				}
+			}
+
+			for (Header v : values) {
+				if (v != null) {
+					defaultEntries.add(v);
+				}
+			}
+		}
+
+		return this;
+	}
+
+	/**
+	 * Replaces the first occurrence of the headers with the same name.
+	 *
+	 * <p>
+	 * If no header with the same name is found the given header is added to the end of the list.
+	 *
+	 * @param values The default headers to set.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	public HeaderListBuilder setDefault(HeaderList values) {
+		if (values != null)
+			setDefault(values.entries);
+		return this;
 	}
 
 	private boolean isResolving() {
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/BasicPartIterator.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/BasicPartIterator.java
new file mode 100644
index 0000000..6e283b7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/BasicPartIterator.java
@@ -0,0 +1,97 @@
+// ***************************************************************************************************************************
+// * 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.part;
+
+import static org.apache.juneau.assertions.Assertions.*;
+
+import java.util.NoSuchElementException;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Basic implementation of a {@link PartIterator}.
+ *
+ * {@review}
+ */
+@NotThreadSafe
+public class BasicPartIterator implements PartIterator {
+
+	private final Part[] entries;
+	private final String name;
+	private final boolean caseInsensitive;
+
+	private int currentIndex;
+
+	/**
+	 * Creates a new part iterator.
+	 *
+	 * @param parts An array of parts over which to iterate.
+	 * @param name The name of the parts over which to iterate, or <jk>null</jk> for all.
+	 * @param caseInsensitive Use case-insensitive matching for part name.
+	 */
+	public BasicPartIterator(Part[] parts, String name, boolean caseInsensitive) {
+		this.entries = assertArgNotNull("parts", parts);
+		this.name = name;
+		this.caseInsensitive = caseInsensitive;
+		this.currentIndex = findNext(-1);
+	}
+
+	private int findNext(int pos) {
+
+		int from = pos;
+
+		int to = entries.length - 1;
+		boolean found = false;
+		while (!found && (from < to)) {
+			from++;
+			found = filter(from);
+		}
+
+		return found ? from : -1;
+	}
+
+	private boolean filter(int index) {
+		return (name == null) || eq(name, entries[index].getName());
+	}
+
+	@Override /* Iterator */
+	public boolean hasNext() {
+		return (currentIndex >= 0);
+	}
+
+	@Override /* Iterator */
+	public Part next() throws NoSuchElementException {
+
+		int current = currentIndex;
+
+		if (current < 0)
+			throw new NoSuchElementException("Iteration already finished.");
+
+		currentIndex = findNext(current);
+
+		return entries[current];
+	}
+
+	/**
+	 * Not supported.
+	 */
+	@Override /* Iterator */
+	public void remove() throws UnsupportedOperationException {
+		throw new UnsupportedOperationException("Remove is not supported.");
+	}
+
+	private boolean eq(String s1, String s2) {
+		return StringUtils.eq(caseInsensitive, s1, s2);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartBeanMeta.java
similarity index 60%
copy from juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartBeanMeta.java
index a3978f3..2b26377 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/header/HeaderBeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartBeanMeta.java
@@ -10,77 +10,78 @@
 // * "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.header;
+package org.apache.juneau.http.part;
 
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.lang.reflect.*;
 import java.util.concurrent.*;
 
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.reflect.*;
 
 /**
- * Holds metadata about header beans (POJOs that get serialized as HTTP headers).
- * {@reviewed}
+ * Holds metadata about http part beans (POJOs that get serialized as HTTP parts such as form data or query parameters).
+ * {@review}
  *
  * <p>
- * Header beans are typically annotated with {@link org.apache.juneau.http.annotation.Header @Header} although it's not an
- * absolute requirement.
+ * HTTP part beans are typically annotated with {@link Query @Query}, {@link FormData @FormData}, or {@link Path @Path}
+ * although it's not an absolute requirement.
  *
  * <p>
- * Header beans must have one of the following public constructors:
+ * HTTP part beans must have one of the following public constructors:
  * <ul>
- * 	<li><c>X(String <jv>headerValue</jv>)</c>
- * 	<li><c>X(Object <jv>headerValue</jv>)</c>
- * 	<li><c>X(String <jv>headerName</jv>, String <jv>headerValue</jv>)</c>
- * 	<li><c>X(String <jv>headerName</jv>, Object <jv>headerValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(String <jv>partValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(Object <jv>partValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(String <jv>partName</jv>, String <jv>partValue</jv>)</c>
+ * 	<li><c><jk>public</jk> X(String <jv>partName</jv>, Object <jv>partValue</jv>)</c>
  * </ul>
  *
  * <p>
  * <h5 class='figure'>Example</h5>
  * <p class='bcode w800'>
- * 	<jc>// Our header bean.</jc>
- * 	<ja>@Header</ja>(<js>"Foo"</js>)
- * 	<jk>public class</jk> FooHeader <jk>extends</jk> BasicStringHeader {
+ * 	<jc>// Our part bean.</jc>
+ * 	<ja>@Query</ja>(<js>"foo"</js>)
+ * 	<jk>public class</jk> FooPart <jk>extends</jk> BasicStringPart {
  *
- * 		<jk>public</jk> FooHeader(String <jv>headerValue</jv>) {
- * 			<jk>super</jk>(<js>"Foo"</js>, <jv>headerValue</jv>);
+ * 		<jk>public</jk> FooPart(String <jv>partValue</jv>) {
+ * 			<jk>super</jk>(<js>"foo"</js>, <jv>partValue</jv>);
  * 		}
  *  }
  *
- *  <jc>// Code to retrieve a header bean from a header list in a request.</jc>
- *  HeaderList <jv>headers</jv> = <jv>httpRequest</jv>.getHeaders();
- *  FooHeader <jv>foo</jv> = <jv>headers</jv>.get(FooHeader.<jk>class</jk>);
+ *  <jc>// Code to retrieve a part bean from a part list in a request.</jc>
+ *  PartList <jv>parts</jv> = <jv>httpRequest</jv>.getFormDataList();
+ *  FooPart <jv>foo</jv> = <jv>parts</jv>.get(FooPart.<jk>class</jk>);
  * </p>
  *
- * @param <T> The header bean type.
+ * @param <T> The HTTP part bean type.
  */
-public class HeaderBeanMeta<T> {
+public class PartBeanMeta<T> {
 
-	private static final ConcurrentHashMap<Class<?>,HeaderBeanMeta<?>> CACHE = new ConcurrentHashMap<>();
+	private static final ConcurrentHashMap<Class<?>,PartBeanMeta<?>> CACHE = new ConcurrentHashMap<>();
 
 	private final Class<T> type;
 	private final Constructor<T> constructor;
 	private final HttpPartSchema schema;
 
 	/**
-	 * Finds the header bean meta for the specified type.
+	 * Finds the part bean meta for the specified type.
 	 *
-	 * @param type The header bean type.
+	 * @param type The part bean type.
 	 * @return The metadata, or <jk>null</jk> if a valid constructor could not be found.
 	 */
 	@SuppressWarnings("unchecked")
-	public static <T> HeaderBeanMeta<T> of(Class<T> type) {
-		HeaderBeanMeta<?> m = CACHE.get(type);
+	public static <T> PartBeanMeta<T> of(Class<T> type) {
+		PartBeanMeta<?> m = CACHE.get(type);
 		if (m == null) {
-			m = new HeaderBeanMeta<>(type);
+			m = new PartBeanMeta<>(type);
 			CACHE.put(type, m);
 		}
-		return (HeaderBeanMeta<T>)m;
+		return (PartBeanMeta<T>)m;
 	}
 
-	private HeaderBeanMeta(Class<T> type) {
+	private PartBeanMeta(Class<T> type) {
 		this.type = type;
 
 		ClassInfo ci = ClassInfo.of(type);
@@ -94,14 +95,24 @@ public class HeaderBeanMeta<T> {
 			cci = ci.getPublicConstructor(String.class, Object.class);
 		constructor = cci == null ? null : cci.inner();
 
-		this.schema = HttpPartSchema.create(org.apache.juneau.http.annotation.Header.class, type);
+		if (ci.hasAnnotation(Query.class))
+			this.schema = HttpPartSchema.create(Query.class, type);
+		else if (ci.hasAnnotation(FormData.class))
+			this.schema = HttpPartSchema.create(FormData.class, type);
+		else if (ci.hasAnnotation(Path.class))
+			this.schema = HttpPartSchema.create(Path.class, type);
+		else if (ci.hasAnnotation(ResponseHeader.class))
+			this.schema = HttpPartSchema.create(ResponseHeader.class, type);
+		else
+			this.schema = HttpPartSchema.create(org.apache.juneau.http.annotation.Header.class, type);
 	}
 
 	/**
-	 * Returns schema information about this header.
+	 * Returns schema information about this part.
 	 *
 	 * <p>
-	 * This is information pulled from {@link org.apache.juneau.http.annotation.Header @Header} annotation on the class.
+	 * This is information pulled from {@link Query @Query}, {@link FormData @FormData}, or {@link Path @Path} annotations
+	 * on the class.
 	 *
 	 * @return The schema information.
 	 */
@@ -110,13 +121,13 @@ public class HeaderBeanMeta<T> {
 	}
 
 	/**
-	 * Constructs a header bean with the specified name or value.
+	 * Constructs a part bean with the specified name or value.
 	 *
 	 * <p>
-	 * Can only be used on beans where the header name is known.
+	 * Can only be used on beans where the part name is known.
 	 *
 	 * @param value
-	 * 	The header value.
+	 * 	The part value.
 	 * @return A newly constructed bean.
 	 */
 	public T construct(Object value) {
@@ -124,14 +135,14 @@ public class HeaderBeanMeta<T> {
 	}
 
 	/**
-	 * Constructs a header bean with the specified name or value.
+	 * Constructs a part bean with the specified name or value.
 	 *
 	 * @param name
-	 * 	The header name.
+	 * 	The part name.
 	 * 	<br>If <jk>null</jk>, uses the value pulled from the {@link org.apache.juneau.http.annotation.Header#name() @Header(name)} or
 	 * 	{@link org.apache.juneau.http.annotation.Header#value() @Header(value)} annotations.
 	 * @param value
-	 * 	The header value.
+	 * 	The part value.
 	 * @return A newly constructed bean.
 	 * @throws UnsupportedOperationException If bean could not be constructed (e.g. couldn't find a constructor).
 	 */
diff --git a/juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartIterator.java
similarity index 67%
copy from juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartIterator.java
index 6ca5880..9ce65a7 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartIterator.java
@@ -1,37 +1,31 @@
-// ***************************************************************************************************************************
-// * 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.utils;
-
-import static org.apache.juneau.assertions.Assertions.*;
-import static org.junit.runners.MethodSorters.*;
-
-import org.apache.juneau.internal.*;
-import org.junit.*;
-
-@FixMethodOrder(NAME_ASCENDING)
-public class ArrayBuilder_Test {
-
-	@Test
-	public void a01_basic() {
-		ArrayBuilder<String> x = ArrayBuilder.create(String.class, 2);
-		assertObject(x.toArray()).asJson().is("[]");
-		x.add(null);
-		assertObject(x.toArray()).asJson().is("[]");
-		x.add("a");
-		assertObject(x.toArray()).asJson().is("['a']");
-		x.add("b");
-		x.add(null);
-		assertObject(x.toArray()).asJson().is("['a','b']");
-		assertThrown(()->x.add("c")).isType(ArrayIndexOutOfBoundsException.class);
-	}
-}
+// ***************************************************************************************************************************
+// * 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.part;
+
+import java.util.Iterator;
+
+/**
+ * A type-safe iterator for {@link Part} objects.
+ *
+ * {@review}
+ */
+public interface PartIterator extends Iterator<Part> {
+
+	/**
+	 * Indicates whether there is another part in this iteration.
+	 *
+	 * @return <jk>true</jk> if there is another part.
+	 */
+	@Override /* Iterator */
+	boolean hasNext();
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartList.java
index 75dde84..0ca3416 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartList.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartList.java
@@ -12,26 +12,153 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.http.part;
 
-import static java.util.Collections.*;
+import static org.apache.juneau.assertions.Assertions.*;
 import static org.apache.juneau.internal.StringUtils.*;
-import static java.util.Arrays.*;
 
 import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
 
+import org.apache.http.*;
+import org.apache.http.util.*;
 import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.http.HttpParts;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.svl.*;
 
 /**
- * An unmodifiable list of HTTP parts.
+ * An immutable list of HTTP parts (form-data, query-parameters, path-parameters).
+ * {@review}
+ *
+ * <h5 class='figure'>Example</h5>
+ * <p class='bcode w800'>
+ * 	PartList <jv>parts</jv> = PartList
+ * 		.<jsm>create</jsm>()
+ * 		.append(MyPart.<jsm>of</jsm>("foo"))
+ * 		.append(<js>"Bar"</js>, ()-><jsm>getDynamicValueFromSomewhere</jsm>())
+ * 		.build();
+ * </p>
+ *
+ * <p>
+ * Convenience creators are provided for creating lists with minimal code:
+ * <p class='bcode w800'>
+ * 	PartList <jv>parts</jv> = PartList.<jsm>of</jsm>(BasicIntegerPart.of(<js>"foo"</js>, 1));
+ * </p>
+ *
+ * <p>
+ * Part lists are immutable, but can be appended to using the {@link #copy()} method:
+ * <p class='bcode w800'>
+ * 	parts = parts
+ * 		.copy()
+ * 		.append(BasicIntegerPart.<jsm>of</jsm>(<js>"foo"</js>, 1))
+ * 		.build();
+ * </p>
+ *
+ * <p>
+ * Static methods are provided on {@link HttpParts} to further simplify creation of part lists.
+ * <p class='bcode w800'>
+ * 	<jk>import static</jk> org.apache.juneau.http.HttpParts.*;
+ *
+ * 	PartList <jv>parts</jv> = <jsm>partList</jsm>(<jsm>integerPart</jsm>(<js>"foo"</js>, 1), <jsm>booleanPart</jsm>(<js>"bar"<js>, <jk>false</jk>));
+ * </p>
+ *
+ * <p>
+ * The builder class supports setting default part values (i.e. add a part to the list if it isn't otherwise in the list).
+ * Note that this is different from simply setting a value twice as using default values will not overwrite existing
+ * parts.
+ * <br>The following example notes the distinction:
+ *
+ * <p class='bcode w800'>
+ * 	<jv>parts</jv> = PartList
+ * 		.<jsm>create</jsm>()
+ * 		.set(<js>"Foo"</js>, <js>"bar"</js>)
+ * 		.set(<js>"Foo"</js>, <js>"baz"</js>)
+ * 		.build();
+ * 	<jsm>assertObject</jsm>(<jv>parts</jv>).isString(<js>"foo=baz"</js>);
+ *
+ * 	<jv>parts</jv> = PartList
+ * 		.create()
+ * 		.set(<js>"Foo"</js>, <js>"bar"</js>)
+ * 		.setDefault(<js>"Foo"</js>, <js>"baz"</js>)
+ * 		.build();
+ * 	<jsm>assertObject</jsm>(<jv>parts</jv>).isString(<js>"foo=bar"</js>);
+ * </p>
+ *
+ * <p>
+ * Various methods are provided for iterating over the parts in this list to avoid array copies.
+ * <ul class='javatree'>
+ * 	<li class='jm'>{@link #forEach(Consumer)} / {@link #forEach(String,Consumer)} - Use consumers to process parts.
+ * 	<li class='jm'>{@link #iterator()} / {@link #iterator(String)} - Use an {@link PartIterator} to process parts.
+ * 	<li class='jm'>{@link #stream()} / {@link #stream(String)} - Use a stream.
+ * </ul>
+ * <p>
+ * In general, try to use these over the {@link #getAll()}/{@link #getAll(String)} methods that require array copies.
+ *
+ * <p>
+ * The {@link #get(String)} method is special in that it will collapse multiple parts with the same name into
+ * a single comma-delimited list (see <a href='https://tools.ietf.org/html/rfc2616#section-4.2'>RFC 2616 Section 4.2</a> for rules).
+ *
+ * <p>
+ * The {@link #get(Class)} and {@link #get(String, Class)} methods are provided for working with {@link FormData}/{@link Query}/{@link Path}/{@link FormData}-annotated
+ * beans.
+ *
+ * <h5 class='figure'>Example</h5>
+ * <p class='bcode w800'>
+ * 	MyQueryBean <jv>foo</jv> = <jv>parts</jv>.get(MyQueryBean.<jk>class</jk>);
+ * </p>
+ *
+ * <p>
+ * By default, part names are treated as case-sensitive.  This can be changed using the {@link PartListBuilder#caseInsensitive()}
+ * method.
+ *
+ * <p>
+ * A {@link VarResolver} can be associated with this builder to create part values with embedded variables that
+ * are resolved at runtime.
+ *
+ * <h5 class='figure'>Example</h5>
+ * <p class='bcode w800'>
+ * 	<jc>// Create a part list with dynamically-resolving values pulled from a system property.</jc>
+ *
+ * 	System.<jsm>setProperty</jsm>(<js>"foo"</js>, <js>"bar"</js>);
+ *
+ * 	PartList <jv>parts</jv> = PartList
+ * 		.<jsm>create</jsm>()
+ * 		.resolving()
+ * 		.append(<js>"X1"</js>, <js>"$S{foo}"</js>)
+ * 		.append(<js>"X2"</js>, ()-><js>"$S{foo}"</js>)
+ * 		.build();
+ *
+ * 	<jsm>assertObject</jsm>(<jv>parts</jv>).isString(<js>"X1=bar&X2=bar"</js>);
+ * </p>
+ *
+ * <p>
+ * The {@link PartList} object can be extended to defined pre-packaged lists of parts which can be used in various
+ * annotations throughout the framework.
+ *
+ * <h5 class='figure'>Example</h5>
+ * <p class='bcode w800'>
+ * 	<jc>// A predefined list of parts.</jc>
+ * 	<jk>public class</jk> MyPartList <jk>extends</jk> PartList {
+ * 		<jk>public</jk> MyPartList() {
+ * 			<jk>super</jk>(BasicIntegerPart.<jsm>of</jsm>(<js>"foo"</js>,1), BasicBooleanPart.<jsm>of</jsm>(<js>"bar"</js>,<jk>false</jk>);
+ * 		}
+ * 	}
  */
-public class PartList implements Iterable<Part> {
+@ThreadSafe
+public class PartList {
+
+	private static final Part[] EMPTY_ARRAY = new Part[0];
 
 	/** Represents no part supplier in annotations. */
 	public static final class Null extends PartList {}
 
 	/** Predefined instance. */
-	public static final PartList EMPTY = create().build();
+	public static final PartList EMPTY = new PartList();
 
-	final List<Part> parts;
+	final Part[] entries;
+	final boolean caseInsensitive;
 
 	/**
 	 * Instantiates a new builder for this bean.
@@ -45,7 +172,10 @@ public class PartList implements Iterable<Part> {
 	/**
 	 * Creates a new {@link PartList} initialized with the specified parts.
 	 *
-	 * @param parts The parts to add to the list.  Can be <jk>null</jk>.  <jk>null</jk> entries are ignored.
+	 * @param parts
+	 * 	The parts to add to the list.
+	 * 	<br>Can be <jk>null</jk>.
+	 * 	<br><jk>null</jk> entries are ignored.
 	 * @return A new unmodifiable instance, never <jk>null</jk>.
 	 */
 	public static PartList of(List<Part> parts) {
@@ -55,16 +185,23 @@ public class PartList implements Iterable<Part> {
 	/**
 	 * Creates a new {@link PartList} initialized with the specified parts.
 	 *
-	 * @param parts The parts to add to the list.  <jk>null</jk> entries are ignored.
+	 * @param parts
+	 * 	The parts to add to the list.
+	 * 	<br><jk>null</jk> entries are ignored.
 	 * @return A new unmodifiable instance, never <jk>null</jk>.
 	 */
 	public static PartList of(Part...parts) {
-		return parts == null || parts.length == 0 ? EMPTY : new PartList(asList(parts));
+		return parts == null || parts.length == 0 ? EMPTY : new PartList(parts);
 	}
 
 	/**
 	 * Creates a new {@link PartList} initialized with the specified name/value pairs.
 	 *
+	 * <h5 class='figure'>Example</h5>
+	 * <p class='bcode w800'>
+	 * 	PartList <jv>parts</jv> = PartList.<jsm>ofPairs</jsm>(<js>"foo"</js>, 1, <js>"bar"</js>, <jk>true</jk>);
+	 * </p>
+	 *
 	 * @param pairs
 	 * 	Initial list of pairs.
 	 * 	<br>Must be an even number of parameters representing key/value pairs.
@@ -72,14 +209,14 @@ public class PartList implements Iterable<Part> {
 	 * @return A new instance.
 	 */
 	public static PartList ofPairs(Object...pairs) {
-		if (pairs.length == 0)
+		if (pairs == null || pairs.length == 0)
 			return EMPTY;
 		if (pairs.length % 2 != 0)
 			throw new BasicRuntimeException("Odd number of parameters passed into PartList.ofPairs()");
-		PartListBuilder b = create();
+		ArrayBuilder<Part> b = ArrayBuilder.create(Part.class, pairs.length / 2, true);
 		for (int i = 0; i < pairs.length; i+=2)
-			b.add(stringify(pairs[i]), pairs[i+1]);
-		return new PartList(b);
+			b.add(BasicPart.of(stringify(pairs[i]), pairs[i+1]));
+		return new PartList(b.toArray());
 	}
 
 	/**
@@ -88,33 +225,65 @@ public class PartList implements Iterable<Part> {
 	 * @param builder The builder containing the settings for this bean.
 	 */
 	public PartList(PartListBuilder builder) {
-		this.parts = new ArrayList<>(builder.parts);
+		if (builder.defaultEntries == null) {
+			entries = builder.entries.toArray(new Part[builder.entries.size()]);
+		} else {
+			ArrayBuilder<Part> l = ArrayBuilder.create(Part.class, builder.entries.size() + builder.defaultEntries.size(), true);
+
+			for (int i = 0, j = builder.entries.size(); i < j; i++)
+				l.add(builder.entries.get(i));
+
+			for (int i1 = 0, j1 = builder.defaultEntries.size(); i1 < j1; i1++) {
+				Part x = builder.defaultEntries.get(i1);
+				boolean exists = false;
+				for (int i2 = 0, j2 = builder.entries.size(); i2 < j2 && ! exists; i2++)
+					exists = eq(builder.entries.get(i2).getName(), x.getName());
+				if (! exists)
+					l.add(x);
+			}
+
+			entries = l.toArray();
+		}
+		this.caseInsensitive = builder.caseInsensitive;
 	}
 
 	/**
 	 * Constructor.
 	 *
-	 * @param parts The initial list of parts.  <jk>null</jk> entries are ignored.
+	 * @param parts
+	 * 	The parts to add to the list.
+	 * 	<br>Can be <jk>null</jk>.
+	 * 	<br><jk>null</jk> entries are ignored.
 	 */
 	protected PartList(List<Part> parts) {
-		if (parts == null || parts.isEmpty())
-			this.parts = emptyList();
-		else {
-			List<Part> l = new ArrayList<>();
-			for (int i = 0; i < parts.size(); i++) {
-				Part x = parts.get(i);
-				if (x != null)
-					l.add(x);
-			}
-			this.parts = unmodifiableList(l);
-		}
+		ArrayBuilder<Part> l = ArrayBuilder.create(Part.class, parts.size(), true);
+		for (int i = 0, j = parts.size(); i < j; i++)
+			l.add(parts.get(i));
+		entries = l.toArray();
+		caseInsensitive = false;
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param parts
+	 * 	The parts to add to the list.
+	 * 	<br><jk>null</jk> entries are ignored.
+	 */
+	protected PartList(Part...parts) {
+		ArrayBuilder<Part> l = ArrayBuilder.create(Part.class, parts.length, true);
+		for (int i = 0; i < parts.length; i++)
+			l.add(parts[i]);
+		entries = l.toArray();
+		caseInsensitive = false;
 	}
 
 	/**
 	 * Default constructor.
 	 */
 	protected PartList() {
-		this.parts = emptyList();
+		entries = EMPTY_ARRAY;
+		caseInsensitive = false;
 	}
 
 	/**
@@ -127,97 +296,319 @@ public class PartList implements Iterable<Part> {
 	}
 
 	/**
-	 * Gets all of the headers with the given name.
+	 * Gets a part representing all of the part values with the given name.
+	 *
+	 * <p>
+	 * If more that one part with the given name exists the values will be combined with <js>", "</js> as per
+	 * <a href='https://tools.ietf.org/html/rfc2616#section-4.2'>RFC 2616 Section 4.2</a>.
+	 *
+	 * @param name The part name.
+	 * @return A part with a condensed value, or {@link Optional#empty()} if no parts by the given name are present
+	 */
+	public Optional<Part> get(String name) {
+
+		Part first = null;
+		List<Part> rest = null;
+		for (int i = 0; i < entries.length; i++) {
+			Part x = entries[i];
+			if (eq(x.getName(), name)) {
+				if (first == null)
+					first = x;
+				else {
+					if (rest == null)
+						rest = new ArrayList<>();
+					rest.add(x);
+				}
+			}
+		}
+
+		if (first == null)
+			return Optional.empty();
+
+		if (rest == null)
+			return Optional.of(first);
+
+		CharArrayBuffer sb = new CharArrayBuffer(128);
+		sb.append(first.getValue());
+		for (int i = 0; i < rest.size(); i++) {
+			sb.append(',');
+			sb.append(rest.get(i).getValue());
+		}
+
+		return Optional.of(new BasicPart(name, sb.toString()));
+	}
+
+	/**
+	 * Gets a part representing all of the part values with the given name.
+	 *
+	 * <p>
+	 * If more that one part with the given name exists the values will be combined with <js>", "</js> as per
+	 * <a href='https://tools.ietf.org/html/rfc2616#section-4.2'>RFC 2616 Section 4.2</a>.
+	 *
+	 * <p>
+	 * The implementation class must have a public constructor taking in one of the following argument lists:
+	 * <ul>
+	 * 	<li><c>X(String <jv>value</jv>)</c>
+	 * 	<li><c>X(Object <jv>value</jv>)</c>
+	 * 	<li><c>X(String <jv>name</jv>, String <jv>value</jv>)</c>
+	 * 	<li><c>X(String <jv>name</jv>, Object <jv>value</jv>)</c>
+	 * </ul>
+	 *
+	 * <h5 class='figure'>Example</h5>
+	 * <p class='bcode w800'>
+	 * 	BasicIntegerPart <jv>age</jv> = partList.get(<js>"age"</js>, BasicIntegerPart.<jk>class</jk>);
+	 * </p>
+	 *
+	 * @param name The part name.
+	 * @param type The part implementation class.
+
+	 * @return A part with a condensed value or <jk>null</jk> if no parts by the given name are present
+	 */
+	public <T> Optional<T> get(String name, Class<T> type) {
+
+		Part first = null;
+		List<Part> rest = null;
+		for (int i = 0; i < entries.length; i++) {
+			Part x = entries[i];
+			if (eq(x.getName(), name)) {
+				if (first == null)
+					first = x;
+				else {
+					if (rest == null)
+						rest = new ArrayList<>();
+					rest.add(x);
+				}
+			}
+		}
+
+		if (first == null)
+			return Optional.empty();
+
+		if (rest == null)
+			return Optional.of(PartBeanMeta.of(type).construct(name, first.getValue()));
+
+		CharArrayBuffer sb = new CharArrayBuffer(128);
+		sb.append(first.getValue());
+		for (int i = 0; i < rest.size(); i++) {
+			sb.append(',');
+			sb.append(rest.get(i).getValue());
+		}
+
+		return Optional.of(PartBeanMeta.of(type).construct(name, sb.toString()));
+	}
+
+	/**
+	 * Gets a part representing all of the part values with the given name.
+	 *
+	 * <p>
+	 * Same as {@link #get(String, Class)} but the part name is pulled from the name or value attribute of the {@link FormData}/{@link Query}/{@link Path} annotation.
+	 *
+	 * <h5 class='figure'>Example</h5>
+	 * <p class='bcode w800'>
+	 * 	Age <jv>age</jv> = partList.get(Age.<jk>class</jk>);
+	 * </p>
+	 *
+	 * @param type The part implementation class.
+	 * @return A part with a condensed value or <jk>null</jk> if no parts by the given name are present
+	 */
+	public <T> Optional<T> get(Class<T> type) {
+		assertArgNotNull("type", type);
+
+		String name = PartBeanMeta.of(type).getSchema().getName();
+		if (name == null)
+			throw new BasicIllegalArgumentException("Part name could not be found on bean type ''{0}''", type.getName());
+
+		return get(name, type);
+	}
+
+	/**
+	 * Gets all of the parts with the given name.
 	 *
 	 * <p>
-	 * The returned array maintains the relative order in which the headers were added.
+	 * The returned array maintains the relative order in which the parts were added.
 	 *
 	 * <p>
-	 * Part name comparison is case sensitive.
+	 * Part name comparison is case insensitive.
 	 *
-	 * @param name The header name.
+	 * @param name The part name.
 	 *
-	 * @return An array containing all matching headers, or an empty array if none are found.
+	 * @return An array containing all matching parts, or an empty array if none are found.
 	 */
-	public List<Part> get(String name) {
+	public Part[] getAll(String name) {
 		List<Part> l = null;
-		for (int i = 0; i < parts.size(); i++) {  // See HTTPCORE-361
-			Part x = parts.get(i);
-			if (x.getName().equals(name)) {
+		for (int i = 0; i < entries.length; i++) {
+			Part x = entries[i];
+			if (eq(x.getName(), name)) {
 				if (l == null)
 					l = new ArrayList<>();
 				l.add(x);
 			}
 		}
-		return l == null ? emptyList() : l;
+		return l == null ? EMPTY_ARRAY : l.toArray(new Part[l.size()]);
+	}
+
+	/**
+	 * Returns the number of parts in this list.
+	 *
+	 * @return The number of parts in this list.
+	 */
+	public int size() {
+		return entries.length;
 	}
 
 	/**
-	 * Gets the first header with the given name.
+	 * Gets the first part with the given name.
 	 *
 	 * <p>
-	 * Part name comparison is case sensitive.
+	 * Part name comparison is case insensitive.
 	 *
-	 * @param name The header name.
-	 * @return The first matching header, or <jk>null</jk> if not found.
+	 * @param name The part name.
+	 * @return The first matching part, or <jk>null</jk> if not found.
 	 */
-	public Part getFirst(String name) {
-		for (int i = 0; i < parts.size(); i++) {  // See HTTPCORE-361
-			Part x = parts.get(i);
-			if (x.getName().equals(name))
-				return x;
+	public Optional<Part> getFirst(String name) {
+		for (int i = 0; i < entries.length; i++) {
+			Part x = entries[i];
+			if (eq(x.getName(), name))
+				return Optional.of(x);
 		}
-		return null;
+		return Optional.empty();
 	}
 
 	/**
-	 * Gets the last header with the given name.
+	 * Gets the last part with the given name.
 	 *
 	 * <p>
-	 * Part name comparison is case sensitive.
+	 * Part name comparison is case insensitive.
 	 *
-	 * @param name The header name.
-	 * @return The last matching header, or <jk>null</jk> if not found.
+	 * @param name The part name.
+	 * @return The last matching part, or <jk>null</jk> if not found.
 	 */
-	public Part getLast(String name) {
-		for (int i = parts.size() - 1; i >= 0; i--) {
-			Part x = parts.get(i);
-			if (x.getName().equals(name))
-				return x;
+	public Optional<Part> getLast(String name) {
+		for (int i = entries.length - 1; i >= 0; i--) {
+			Part x = entries[i];
+			if (eq(x.getName(), name))
+				return Optional.of(x);
 		}
-		return null;
+		return Optional.empty();
 	}
 
 	/**
-	 * Gets all of the headers contained within this list.
+	 * Gets all of the parts contained within this list.
 	 *
-	 * @return An array containing all the parts within this list, or an empty list if no parts are present.
+	 * @return An array of all the parts in this list, or an empty array if no parts are present.
 	 */
-	public List<Part> getAll() {
-		return unmodifiableList(parts);
+	public Part[] getAll() {
+		return entries.length == 0 ? EMPTY_ARRAY : Arrays.copyOf(entries, entries.length);
 	}
 
 	/**
-	 * Tests if headers with the given name are contained within this list.
+	 * Tests if parts with the given name are contained within this list.
 	 *
 	 * <p>
-	 * Part name comparison is case sensitive.
+	 * Part name comparison is case insensitive.
 	 *
-	 * @param name The header name.
-	 * @return <jk>true</jk> if at least one header with the name is present.
+	 * @param name The part name.
+	 * @return <jk>true</jk> if at least one part with the name is present.
 	 */
 	public boolean contains(String name) {
-		for (int i = 0; i < parts.size(); i++) {  // See HTTPCORE-361
-			Part x = parts.get(i);
-			if (x.getName().equals(name))
+		for (int i = 0; i < entries.length; i++) {
+			Part x = entries[i];
+			if (eq(x.getName(), name))
 				return true;
 		}
 		return false;
 	}
 
-	@Override /* Iterable */
-	public Iterator<Part> iterator() {
-		return getAll().iterator();
+	/**
+	 * Returns an iterator over this list of parts.
+	 *
+	 * @return A new iterator over this list of parts.
+	 */
+	public PartIterator iterator() {
+		return new BasicPartIterator(entries, null, caseInsensitive);
+	}
+
+	/**
+	 * Returns an iterator over the parts with a given name in this list.
+	 *
+	 * @param name The name of the parts over which to iterate, or <jk>null</jk> for all parts
+	 *
+	 * @return A new iterator over the matching parts in this list.
+	 */
+	public PartIterator iterator(String name) {
+		return new BasicPartIterator(entries, name, caseInsensitive);
+	}
+
+	/**
+	 * Performs an operation on the parts of this list.
+	 *
+	 * <p>
+	 * This is the preferred method for iterating over parts as it does not involve
+	 * creation or copy of lists/arrays.
+	 *
+	 * @param c The consumer.
+	 * @return This object (for method chaining).
+	 */
+	public PartList forEach(Consumer<Part> c) {
+		for (int i = 0; i < entries.length; i++)
+			c.accept(entries[i]);
+		return this;
+	}
+
+	/**
+	 * Performs an operation on the parts with the specified name in this list.
+	 *
+	 * <p>
+	 * This is the preferred method for iterating over parts as it does not involve
+	 * creation or copy of lists/arrays.
+	 *
+	 * @param name The part name.
+	 * @param c The consumer.
+	 * @return This object (for method chaining).
+	 */
+	public PartList forEach(String name, Consumer<Part> c) {
+		for (int i = 0; i < entries.length; i++)
+			if (eq(name, entries[i].getName()))
+				c.accept(entries[i]);
+		return this;
+	}
+
+	/**
+	 * Returns a stream of the parts in this list.
+	 *
+	 * <p>
+	 * This does not involve a copy of the underlying array of <c>Part</c> objects so should perform well.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public Stream<Part> stream() {
+		return Arrays.stream(entries);
+	}
+
+	/**
+	 * Returns a stream of the parts in this list with the specified name.
+	 *
+	 * <p>
+	 * This does not involve a copy of the underlying array of <c>Part</c> objects so should perform well.
+	 *
+	 * @param name The part name.
+	 * @return This object (for method chaining).
+	 */
+	public Stream<Part> stream(String name) {
+		return Arrays.stream(entries).filter(x->eq(name, x.getName()));
+	}
+
+	/**
+	 * Returns the contents of this list as an unmodifiable list of {@link NameValuePair} objects.
+	 *
+	 * @return The contents of this list as an unmodifiable list of {@link NameValuePair} objects.
+	 */
+	public List<NameValuePair> asNameValuePairs() {
+		return Collections.unmodifiableList(Arrays.asList(entries));
+	}
+
+	private boolean eq(String s1, String s2) {
+		return StringUtils.eq(caseInsensitive, s1, s2);
 	}
 
 	/**
@@ -226,8 +617,8 @@ public class PartList implements Iterable<Part> {
 	@Override /* Object */
 	public String toString() {
 		StringBuilder sb = new StringBuilder();
-		for (int i = 0; i < parts.size(); i++) {
-			Part p = parts.get(i);
+		for (int i = 0; i < entries.length; i++) {
+			Part p = entries[i];
 			String v = p.getValue();
 			if (v != null) {
 				if (sb.length() > 0)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartListBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartListBuilder.java
index f37513f..d275e90 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartListBuilder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/part/PartListBuilder.java
@@ -17,19 +17,22 @@ import static org.apache.juneau.internal.StringUtils.*;
 import java.util.*;
 import java.util.function.*;
 
-import org.apache.juneau.httppart.*;
+import org.apache.juneau.annotation.*;
 import org.apache.juneau.internal.*;
-import org.apache.juneau.oapi.*;
 import org.apache.juneau.svl.*;
 
 /**
  * Builder for {@link PartList} objects.
+ * {@review}
  */
 @FluentSetters
+@NotThreadSafe
 public class PartListBuilder {
 
-	final List<Part> parts;
-	private volatile VarResolver varResolver;
+	final List<Part> entries;
+	List<Part> defaultEntries;
+	private VarResolver varResolver;
+	boolean caseInsensitive = false;
 
 	/** Predefined instance. */
 	private static final PartList EMPTY = new PartList();
@@ -38,7 +41,7 @@ public class PartListBuilder {
 	 * Constructor.
 	 */
 	public PartListBuilder() {
-		parts = new ArrayList<>();
+		entries = new ArrayList<>();
 	}
 
 	/**
@@ -47,8 +50,10 @@ public class PartListBuilder {
 	 * @param copyFrom The bean to copy from.
 	 */
 	public PartListBuilder(PartList copyFrom) {
-		parts = new ArrayList<>(copyFrom.parts);
-
+		entries = new ArrayList<>(copyFrom.entries.length);
+		for (int i = 0; i < copyFrom.entries.length; i++)
+			entries.add(copyFrom.entries[i]);
+		caseInsensitive = copyFrom.caseInsensitive;
 	}
 
 	/**
@@ -57,7 +62,7 @@ public class PartListBuilder {
 	 * @return A new {@link PartList} bean.
 	 */
 	public PartList build() {
-		return parts.isEmpty() ? EMPTY : new PartList(this);
+		return entries.isEmpty() && defaultEntries == null ? EMPTY : new PartList(this);
 	}
 
 	/**
@@ -66,9 +71,14 @@ public class PartListBuilder {
 	 * <p>
 	 * Resolves variables in part values when using the following methods:
 	 * <ul>
-	 * 	<li class='jm'>{@link #add(String, Object) add(String,Object)}
-	 * 	<li class='jm'>{@link #add(String, Supplier) add(String,Supplier&lt;?&gt;)}
-	 * 	<li class='jm'>{@link #add(String, Object, HttpPartType, HttpPartSerializerSession, HttpPartSchema, boolean) add(String,Object,HttpPartType,HttpPartSerializerSession,HttpPartSchema,boolean)}
+	 * 	<li class='jm'>{@link #append(String, Object) append(String,Object)}
+	 * 	<li class='jm'>{@link #append(String, Supplier) append(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #prepend(String, Object) prepend(String,Object)}
+	 * 	<li class='jm'>{@link #prepend(String, Supplier) prepend(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #set(String, Object) set(String,Object)}
+	 * 	<li class='jm'>{@link #set(String, Supplier) set(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #setDefault(String, Object) set(String,Object)}
+	 * 	<li class='jm'>{@link #setDefault(String, Supplier) set(String,Supplier&lt;?&gt;)}
 	 * </ul>
 	 *
 	 * <p>
@@ -86,9 +96,14 @@ public class PartListBuilder {
 	 * <p>
 	 * Resolves variables in part values when using the following methods:
 	 * <ul>
-	 * 	<li class='jm'>{@link #add(String, Object) add(String,Object)}
-	 * 	<li class='jm'>{@link #add(String, Supplier) add(String,Supplier&lt;?&gt;)}
-	 * 	<li class='jm'>{@link #add(String, Object, HttpPartType, HttpPartSerializerSession, HttpPartSchema, boolean) add(String,Object,HttpPartType,HttpPartSerializerSession,HttpPartSchema,boolean)}
+	 * 	<li class='jm'>{@link #append(String, Object) append(String,Object)}
+	 * 	<li class='jm'>{@link #append(String, Supplier) append(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #prepend(String, Object) prepend(String,Object)}
+	 * 	<li class='jm'>{@link #prepend(String, Supplier) prepend(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #set(String, Object) set(String,Object)}
+	 * 	<li class='jm'>{@link #set(String, Supplier) set(String,Supplier&lt;?&gt;)}
+	 * 	<li class='jm'>{@link #setDefault(String, Object) set(String,Object)}
+	 * 	<li class='jm'>{@link #setDefault(String, Supplier) set(String,Supplier&lt;?&gt;)}
 	 * </ul>
 	 *
 	 * @param varResolver The variable resolver to use for resolving variables.
@@ -100,59 +115,72 @@ public class PartListBuilder {
 	}
 
 	/**
+	 * Specifies that the part in this builder should be treated as case-insensitive.
+	 *
+	 * <p>
+	 * The default behavior is case-sensitive.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder caseInsensitive() {
+		caseInsensitive = true;
+		return this;
+	}
+
+	/**
 	 * Removes any parts already in this builder.
 	 *
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
 	public PartListBuilder clear() {
-		parts.clear();
+		entries.clear();
 		return this;
 	}
 
 	/**
-	 * Adds the specified part to the end of the parts in this builder.
+	 * Adds the specified parts to the end of the parts in this builder.
 	 *
-	 * @param value The part to add.  <jk>null</jk> values are ignored.
+	 * @param value The parts to add.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder add(Part value) {
+	public PartListBuilder append(PartList value) {
 		if (value != null)
-			parts.add(value);
+			for (Part x : value.entries)
+				append(x);
 		return this;
 	}
 
 	/**
-	 * Adds the specified part to the end of the parts in this builder.
+	 * Adds the specified parts to the end of the parts in this builder.
 	 *
-	 * @param name The part name.
-	 * @param value The part value.
+	 * @param value The parts to add.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder add(String name, String value) {
-		Part x = isResolving() ? new BasicPart(name, resolver(value)) : new BasicPart(name, value);
-		return add(x);
+	public PartListBuilder append(Part value) {
+		if (value != null)
+			entries.add(value);
+		return this;
 	}
 
 	/**
-	 * Appends the specified part to the end of this list.
+	 * Appends the specified parts to the end of this builder.
 	 *
 	 * <p>
-	 * The part is added as a {@link BasicPart}.
+	 * The parts is added as a {@link BasicPart}.
 	 *
 	 * @param name The part name.
 	 * @param value The part value.
 	 * @return This object (for method chaining).
 	 */
-	public PartListBuilder add(String name, Object value) {
-		Part x = isResolving() ? new BasicPart(name, resolver(value)) : new BasicPart(name, value);
-		return add(x);
+	public PartListBuilder append(String name, Object value) {
+		return append(part(name, value));
 	}
 
 	/**
-	 * Appends the specified part to the end of this list using a value supplier.
+	 * Appends the specified part to the end of this builder using a value supplier.
 	 *
 	 * <p>
 	 * The part is added as a {@link BasicPart}.
@@ -164,30 +192,22 @@ public class PartListBuilder {
 	 * @param value The part value supplier.
 	 * @return This object (for method chaining).
 	 */
-	public PartListBuilder add(String name, Supplier<?> value) {
-		Part x = isResolving() ? new BasicPart(name, resolver(value)) : new BasicPart(name, value);
-		return add(x);
+	public PartListBuilder append(String name, Supplier<?> value) {
+		return append(part(name, value));
 	}
 
 	/**
-	 * Appends the specified part to the end of this list.
+	 * Adds the specified parts to the end of the parts in this builder.
 	 *
-	 * @param name The part name.
-	 * @param value The part value.
-	 * @param type The HTTP part type.
-	 * @param serializer
-	 * 	The serializer to use for serializing the value to a string value.
-	 * @param schema
-	 * 	The schema object that defines the format of the output.
-	 * 	<br>If <jk>null</jk>, defaults to the schema defined on the parser.
-	 * 	<br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
-	 * 	<br>Only used if serializer is schema-aware (e.g. {@link OpenApiSerializer}).
-	 * @param skipIfEmpty If value is a blank string, the value should return as <jk>null</jk>.
+	 * @param values The parts to add.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
-	public PartListBuilder add(String name, Object value, HttpPartType type, HttpPartSerializerSession serializer, HttpPartSchema schema, boolean skipIfEmpty) {
-		Part x = isResolving() ? new SerializedPart(name, resolver(value), type, serializer, schema, skipIfEmpty) : new SerializedPart(name, value, type, serializer, schema, skipIfEmpty);
-		return add(x);
+	@FluentSetter
+	public PartListBuilder append(Part...values) {
+		if (values != null)
+			for (int i = 0; i < values.length; i++)
+				append(values[i]);
+		return this;
 	}
 
 	/**
@@ -197,47 +217,120 @@ public class PartListBuilder {
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder add(Part...values) {
-		for (int i = 0; i < values.length; i++) /* See HTTPCORE-361 */
-			add(values[i]);
+	public PartListBuilder append(List<Part> values) {
+		if (values != null)
+			for (int i = 0, j = values.size(); i < j; i++)
+				append(values.get(i));
 		return this;
 	}
 
 	/**
-	 * Adds the specified parts to the end of the parts in this builder.
+	 * Adds the specified part to the beginning of the parts in this builder.
+	 *
+	 * @param value The part to add.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public PartListBuilder prepend(PartList value) {
+		if (value != null)
+			entries.addAll(0, Arrays.asList(value.entries));
+		return this;
+	}
+
+	/**
+	 * Adds the specified part to the beginning of the parts in this builder.
+	 *
+	 * @param value The part to add.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public PartListBuilder prepend(Part value) {
+		if (value != null)
+			entries.add(0, value);
+		return this;
+	}
+
+	/**
+	 * Appends the specified part to the beginning of this builder.
+	 *
+	 * <p>
+	 * The part is added as a {@link BasicPart}.
+	 *
+	 * @param name The part name.
+	 * @param value The part value.
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder prepend(String name, Object value) {
+		return prepend(part(name, value));
+	}
+
+	/**
+	 * Appends the specified part to the beginning of this builder using a value supplier.
+	 *
+	 * <p>
+	 * The part is added as a {@link BasicPart}.
+	 *
+	 * <p>
+	 * Value is re-evaluated on each call to {@link BasicPart#getValue()}.
+	 *
+	 * @param name The part name.
+	 * @param value The part value supplier.
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder prepend(String name, Supplier<?> value) {
+		return prepend(part(name, value));
+	}
+
+	/**
+	 * Adds the specified parts to the beginning of the parts in this builder.
 	 *
 	 * @param values The parts to add.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder add(List<Part> values) {
-		for (int i = 0; i < values.size(); i++) /* See HTTPCORE-361 */
-			add(values.get(i));
+	public PartListBuilder prepend(Part...values) {
+		if (values != null)
+			prepend(Arrays.asList(values));
 		return this;
 	}
 
 	/**
-	 * Adds the specified parts to the end of the parts in this builder.
+	 * Adds the specified parts to the beginning of the parts in this builder.
 	 *
-	 * @param values The part to add.  <jk>null</jk> values are ignored.
+	 * @param values The parts to add.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
-	public PartListBuilder add(PartList values) {
+	@FluentSetter
+	public PartListBuilder prepend(List<Part> values) {
 		if (values != null)
-			add(values.getAll());
+			entries.addAll(0, values);
 		return this;
 	}
 
 	/**
-	 * Removes the specified parts from this builder.
+	 * Removes the specified part from this builder.
+	 *
+	 * @param value The part to remove.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public PartListBuilder remove(PartList value) {
+		if (value != null)
+			for (int i = 0; i < value.entries.length; i++)
+				remove(value.entries[i]);
+		return this;
+	}
+
+	/**
+	 * Removes the specified part from this builder.
 	 *
-	 * @param value The parts to remove.  <jk>null</jk> values are ignored.
+	 * @param value The part to remove.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
 	public PartListBuilder remove(Part value) {
 		if (value != null)
-			parts.remove(value);
+			entries.remove(value);
 		return this;
 	}
 
@@ -249,7 +342,7 @@ public class PartListBuilder {
 	 */
 	@FluentSetter
 	public PartListBuilder remove(Part...values) {
-		for (int i = 0; i < values.length; i++) /* See HTTPCORE-361 */
+		for (int i = 0; i < values.length; i++)
 			remove(values[i]);
 		return this;
 	}
@@ -262,7 +355,7 @@ public class PartListBuilder {
 	 */
 	@FluentSetter
 	public PartListBuilder remove(List<Part> values) {
-		for (int i = 0; i < values.size(); i++) /* See HTTPCORE-361 */
+		for (int i = 0, j = values.size(); i < j; i++) /* See HTTPCORE-361 */
 			remove(values.get(i));
 		return this;
 	}
@@ -275,14 +368,28 @@ public class PartListBuilder {
 	 */
 	@FluentSetter
 	public PartListBuilder remove(String name) {
-		for (int i = 0; i < parts.size(); i++) /* See HTTPCORE-361 */
-			if (parts.get(i).getName().equals(name))
-				parts.remove(i--);
-		return null;
+		for (int i = 0; i < entries.size(); i++) /* See HTTPCORE-361 */
+			if (eq(entries.get(i).getName(), name))
+				entries.remove(i--);
+		return this;
+	}
+
+	/**
+	 * Removes the part with the specified name from this builder.
+	 *
+	 * @param names The part name.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public PartListBuilder remove(String...names) {
+		if (names != null)
+			for (int i = 0; i < names.length; i++)
+				remove(names[i]);
+		return this;
 	}
 
 	/**
-	 * Replaces the first occurrence of the part with the same name.
+	 * Adds or replaces the part(s) with the same name.
 	 *
 	 * <p>
 	 * If no part with the same name is found the given part is added to the end of the list.
@@ -291,24 +398,31 @@ public class PartListBuilder {
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder update(Part value) {
-		if (value == null)
-			return this;
-
-		for (int i = 0; i < parts.size(); i++) {
-			Part x = parts.get(i);
-			if (x.getName().equals(value.getName())) {
-				parts.set(i, value);
-				return this;
+	public PartListBuilder set(Part value) {
+		if (value != null) {
+			boolean replaced = false;
+			for (int i = 0, j = entries.size(); i < j; i++) {
+				Part x = entries.get(i);
+				if (eq(x.getName(), value.getName())) {
+					if (replaced) {
+						entries.remove(i);
+						j--;
+					} else {
+						entries.set(i, value);
+						replaced = true;
+					}
+				}
 			}
+
+			if (! replaced)
+				entries.add(value);
 		}
 
-		parts.add(value);
 		return this;
 	}
 
 	/**
-	 * Replaces the first occurrence of the parts with the same name.
+	 * Adds or replaces the part(s) with the same name.
 	 *
 	 * <p>
 	 * If no part with the same name is found the given part is added to the end of the list.
@@ -317,112 +431,209 @@ public class PartListBuilder {
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder update(Part...values) {
-		for (int i = 0; i < values.length; i++) /* See HTTPCORE-361 */
-			update(values[i]);
+	public PartListBuilder set(Part...values) {
+		if (values != null)
+			set(Arrays.asList(values));
 		return this;
 	}
 
 	/**
 	 * Replaces the first occurrence of the parts with the same name.
 	 *
-	 * <p>
-	 * If no part with the same name is found the given part is added to the end of the list.
+	 * @param name The part name.
+	 * @param value The part value.
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder set(String name, Object value) {
+		return set(part(name, value));
+	}
+
+	/**
+	 * Replaces the first occurrence of the parts with the same name.
 	 *
-	 * @param values The parts to replace.  <jk>null</jk> values are ignored.
+	 * @param name The part name.
+	 * @param value The part value.
 	 * @return This object (for method chaining).
 	 */
-	@FluentSetter
-	public PartListBuilder update(List<Part> values) {
-		for (int i = 0; i < values.size(); i++) /* See HTTPCORE-361 */
-			update(values.get(i));
-		return this;
+	public PartListBuilder set(String name, Supplier<?> value) {
+		return set(part(name, value));
 	}
 
 	/**
-	 * Sets all of the parts contained within this list overriding any existing parts.
+	 * Replaces the first occurrence of the parts with the same name.
 	 *
 	 * <p>
-	 * The parts are added in the order in which they appear in the array.
+	 * If no part with the same name is found the given part is added to the end of the list.
 	 *
-	 * @param values The parts to set
+	 * @param values The parts to replace.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
 	@FluentSetter
-	public PartListBuilder set(Part...values) {
-		clear();
-		Collections.addAll(parts, values);
+	public PartListBuilder set(List<Part> values) {
+
+		if (values != null) {
+			for (int i1 = 0, j1 = values.size(); i1 < j1; i1++) {
+				Part p = values.get(i1);
+				if (p != null) {
+					for (int i2 = 0, j2 = entries.size(); i2 < j2; i2++) {
+						Part x = entries.get(i2);
+						if (eq(x.getName(), p.getName())) {
+							entries.remove(i2);
+							j2--;
+						}
+					}
+				}
+			}
+
+			for (int i = 0, j = values.size(); i < j; i++) {
+				Part x = values.get(i);
+				if (x != null) {
+					entries.add(x);
+				}
+			}
+		}
+
 		return this;
 	}
 
 	/**
-	 * Sets all of the parts contained within this list overriding any existing parts.
+	 * Replaces the first occurrence of the parts with the same name.
 	 *
 	 * <p>
-	 * The parts are added in the order in which they appear in the list.
+	 * If no part with the same name is found the given part is added to the end of the list.
 	 *
-	 * @param values The parts to set
+	 * @param values The parts to replace.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
-	@FluentSetter
-	public PartListBuilder set(List<Part> values) {
-		clear();
-		parts.addAll(values);
+	public PartListBuilder set(PartList values) {
+		if (values != null)
+			set(values.entries);
 		return this;
 	}
 
 	/**
-	 * Appends or replaces the part values in this list.
+	 * Sets a default value for a part.
 	 *
 	 * <p>
-	 * If the part already exists in this list, it will be replaced with the new value.
-	 * Otherwise it will be appended to the end of this list.
+	 * If no part with the same name is found, the given part is added to the end of the list.
 	 *
-	 * @param values The values to append or replace in this list.
+	 * @param value The default part to set.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
-	public PartListBuilder appendUnique(Part...values) {
-		for (Part h : values) {
+	@FluentSetter
+	public PartListBuilder setDefault(Part value) {
+		if (value != null) {
 			boolean replaced = false;
-			for (ListIterator<Part> li = parts.listIterator(); li.hasNext();) {
-				Part h2 = li.next();
-				if (h2.getName().equals(h.getName())) {
-					li.set(h);
-					replaced = true;
-					break;
+			if (defaultEntries == null)
+				defaultEntries = new ArrayList<>();
+			for (int i = 0, j = defaultEntries.size(); i < j; i++) {
+				Part x = defaultEntries.get(i);
+				if (eq(x.getName(), value.getName())) {
+					if (replaced) {
+						defaultEntries.remove(i);
+						j--;
+					} else {
+						defaultEntries.set(i, value);
+						replaced = true;
+					}
 				}
 			}
+
 			if (! replaced)
-				add(h);
+				defaultEntries.add(value);
 		}
+
+		return this;
+	}
+
+	/**
+	 * Sets default values for one or more parts.
+	 *
+	 * <p>
+	 * If no part with the same name is found the given part is added to the end of the list.
+	 *
+	 * @param values The default parts to set.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	@FluentSetter
+	public PartListBuilder setDefault(Part...values) {
+		if (values != null)
+			setDefault(Arrays.asList(values));
 		return this;
 	}
 
+	/**
+	 * Sets a default value for a part.
+	 *
+	 * @param name The part name.
+	 * @param value The part value.
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder setDefault(String name, Object value) {
+		return setDefault(part(name, value));
+	}
+
+	/**
+	 * Sets a default value for a part.
+	 *
+	 * @param name The part name.
+	 * @param value The part value.
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder setDefault(String name, Supplier<?> value) {
+		return setDefault(part(name, value));
+	}
 
 	/**
-	 * Appends or replaces the part values in this list.
+	 * Sets default values for one or more parts.
 	 *
 	 * <p>
-	 * If the part already exists in this list, it will be replaced with the new value.
-	 * Otherwise it will be appended to the end of this list.
+	 * If no part with the same name is found the given part is added to the end of the list.
 	 *
-	 * @param values The values to append or replace in this list.
+	 * @param values The default parts to set.  <jk>null</jk> values are ignored.
 	 * @return This object (for method chaining).
 	 */
-	public PartListBuilder appendUnique(Collection<Part> values) {
-		for (Part h : values) {
-			boolean replaced = false;
-			for (ListIterator<Part> li = parts.listIterator(); li.hasNext();) {
-				Part h2 = li.next();
-				if (h2.getName().equals(h.getName())) {
-					li.set(h);
-					replaced = true;
-					break;
+	@FluentSetter
+	public PartListBuilder setDefault(List<Part> values) {
+
+		if (values != null) {
+			if (defaultEntries == null)
+				defaultEntries = new ArrayList<>();
+			for (int i1 = 0, j1 = values.size(); i1 < j1; i1++) {
+				Part p = values.get(i1);
+				if (p != null) {
+					for (int i2 = 0, j2 = defaultEntries.size(); i2 < j2; i2++) {
+						Part x = defaultEntries.get(i2);
+						if (eq(x.getName(), p.getName())) {
+							defaultEntries.remove(i2);
+							j2--;
+						}
+					}
+				}
+			}
+
+			for (Part v : values) {
+				if (v != null) {
+					defaultEntries.add(v);
 				}
 			}
-			if (! replaced)
-				add(h);
 		}
+
+		return this;
+	}
+
+	/**
+	 * Replaces the first occurrence of the parts with the same name.
+	 *
+	 * <p>
+	 * If no part with the same name is found the given part is added to the end of the list.
+	 *
+	 * @param values The default parts to set.  <jk>null</jk> values are ignored.
+	 * @return This object (for method chaining).
+	 */
+	public PartListBuilder setDefault(PartList values) {
+		if (values != null)
+			setDefault(values.entries);
 		return this;
 	}
 
@@ -431,7 +642,7 @@ public class PartListBuilder {
 	}
 
 	private Supplier<Object> resolver(Object input) {
-		return ()->(varResolver == null ? unwrap(input) : varResolver.resolve(stringify(unwrap(input))));
+		return ()->varResolver.resolve(stringify(unwrap(input)));
 	}
 
 	private Object unwrap(Object o) {
@@ -440,6 +651,17 @@ public class PartListBuilder {
 		return o;
 	}
 
+	private Part part(String name, Object value) {
+		return isResolving() ? new BasicPart(name, resolver(value)) : new BasicPart(name, value);
+	}
+
+	private Part part(String name, Supplier<?> value) {
+		return isResolving() ? new BasicPart(name, resolver(value)) : new BasicPart(name, value);
+	}
+
+	private boolean eq(String s1, String s2) {
+		return caseInsensitive ? StringUtils.eq(s1, s2) : StringUtils.eqic(s1, s2);
+	}
 	// <FluentSetters>
 
 	// </FluentSetters>
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayBuilder.java
index 9586424..5e1cb23 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayBuilder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayBuilder.java
@@ -28,16 +28,18 @@ public class ArrayBuilder<T> {
 
 	private final T[] array;
 	private int i = 0;
+	private final boolean skipNulls;
 
 	/**
 	 * Creator.
 	 *
 	 * @param elementType The element type.
 	 * @param size The array size.
+	 * @param skipNulls If <jk>true</jk>, <jk>null</jk> values will be ignored.
 	 * @return A new builder object.
 	 */
-	public static <T> ArrayBuilder<T> create(Class<T> elementType, int size) {
-		return new ArrayBuilder<>(elementType, size);
+	public static <T> ArrayBuilder<T> create(Class<T> elementType, int size, boolean skipNulls) {
+		return new ArrayBuilder<>(elementType, size, skipNulls);
 	}
 
 	/**
@@ -45,10 +47,12 @@ public class ArrayBuilder<T> {
 	 *
 	 * @param elementType The element type.
 	 * @param capacity The array size.
+	 * @param skipNulls If <jk>true</jk>, <jk>null</jk> values will be ignored.
 	 */
 	@SuppressWarnings("unchecked")
-	public ArrayBuilder(Class<T> elementType, int capacity) {
+	public ArrayBuilder(Class<T> elementType, int capacity, boolean skipNulls) {
 		array = (T[])Array.newInstance(elementType, capacity);
+		this.skipNulls = skipNulls;
 	}
 
 	/**
@@ -59,7 +63,7 @@ public class ArrayBuilder<T> {
 	 * @throws ArrayIndexOutOfBoundsException if size is exceeded.
 	 */
 	public ArrayBuilder<T> add(T t) {
-		if (t != null)
+		if (!(skipNulls && t == null))
 			array[i++] = t;
 		return this;
 	}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
index 1af5b3f..95b6e74 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
@@ -1259,6 +1259,18 @@ public final class StringUtils {
 	}
 
 	/**
+	 * Tests two strings for equality, but gracefully handles nulls.
+	 *
+	 * @param caseInsensitive Use case-insensitive matching.
+	 * @param s1 String 1.
+	 * @param s2 String 2.
+	 * @return <jk>true</jk> if the strings are equal.
+	 */
+	public static boolean eq(boolean caseInsensitive, String s1, String s2) {
+		return caseInsensitive ? eqic(s1, s2) : eq(s1, s2);
+	}
+
+	/**
 	 * Finds the position where the two strings differ.
 	 *
 	 * @param s1 The first string.
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 5e84e16..3f15530 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -2100,9 +2100,9 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re
 		for (Object o : cp.getList(RESTCLIENT_query, Object.class).orElse(emptyList())) {
 			o = buildBuilders(o, partSerializerSession);
 			if (o instanceof PartList)
-				query.add((PartList)o);
+				query.append((PartList)o);
 			else
-				query.add(BasicPart.cast(o));
+				query.append(BasicPart.cast(o));
 		}
 		this.query = query.build();
 
@@ -2110,9 +2110,9 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re
 		for (Object o : cp.getList(RESTCLIENT_formData, Object.class).orElse(emptyList())) {
 			o = buildBuilders(o, partSerializerSession);
 			if (o instanceof PartList)
-				formData.add((PartList)o);
+				formData.append((PartList)o);
 			else
-				formData.add(BasicPart.cast(o));
+				formData.append(BasicPart.cast(o));
 		}
 		this.formData = formData.build();
 
@@ -2530,7 +2530,7 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re
 			if (body instanceof NameValuePair[])
 				return req.body(new UrlEncodedFormEntity(Arrays.asList((NameValuePair[])body)));
 			if (body instanceof PartList)
-				return req.body(new UrlEncodedFormEntity((PartList)body));
+				return req.body(new UrlEncodedFormEntity(((PartList)body).asNameValuePairs()));
 			if (body instanceof HttpResource)
 				((HttpResource)body).getHeaders().forEach(x-> req.header(x));
 			if (body instanceof HttpEntity) {
@@ -2896,13 +2896,11 @@ public class RestClient extends BeanContext implements HttpClient, Closeable, Re
 
 		RestRequest req = createRequest(toURI(op.getUri(), rootUri), op.getMethod(), op.hasBody());
 
-		headers.forEach(x -> req.header(APPEND, BasicHeader.cast(x)));
+		headers.forEach(x -> req.header(APPEND, x));
 
-		for (Object o : query)
-			req.query(BasicPart.cast(o));
+		query.forEach(x -> req.query(APPEND, x.getName(), x.getValue()));
 
-		for (Object o : formData)
-			req.formData(BasicPart.cast(o));
+		formData.forEach(x -> req.formData(APPEND, x.getName(), x.getValue()));
 
 		onInit(req);
 
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
index e14fe4e..c1aa656 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
@@ -987,16 +987,14 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 * 		<li>A collection or array of anything on this list.
 	 * </ul>
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
 	@SuppressWarnings("rawtypes")
-	public RestRequest paths(Object...params) throws RestCallException {
+	public RestRequest paths(Object...params) {
 		for (Object o : params) {
 			if (BasicPart.canCast(o)) {
 				innerPath(BasicPart.cast(o));
 			} else if (o instanceof PartList) {
-				for (NameValuePair p : (PartList)o)
-					innerPath(p);
+				((PartList)o).forEach(x -> innerPath(x));
 			} else if (o instanceof Collection) {
 				for (Object o2 : (Collection<?>)o)
 					innerPath(BasicPart.cast(o2));
@@ -1010,7 +1008,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 				for (Map.Entry<String,Object> e : toBeanMap(o).entrySet())
 					innerPath(serializedPart(e.getKey(), e.getValue(), PATH, partSerializer, null, null));
 			} else if (o != null) {
-				throw new RestCallException(null, null, "Invalid type passed to paths(): {0}", className(o));
+				throw new BasicRuntimeException("Invalid type passed to paths(): {0}", className(o));
 			}
 		}
 		return this;
@@ -1056,8 +1054,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 		if (BasicPart.canCast(value)) {
 			innerPath(BasicPart.cast(value));
 		} else if (value instanceof PartList) {
-			for (Part o : (PartList)value)
-				innerPath(o);
+			((PartList)value).forEach(x -> innerPath(x));
 		} else if (value instanceof Collection) {
 			for (Object o : (Collection<?>)value)
 				innerPath(BasicPart.cast(o));
@@ -1076,12 +1073,12 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 		return this;
 	}
 
-	private RestRequest innerPath(NameValuePair param) throws RestCallException {
+	private RestRequest innerPath(NameValuePair param) {
 		String path = uriBuilder.getPath();
 		String name = param.getName(), value = param.getValue();
 		String var = "{" + name + "}";
 		if (path.indexOf(var) == -1 && ! name.equals("/*"))
-			throw new RestCallException(null, null, "Path variable {"+name+"} was not found in path.");
+			throw new RuntimeException("Path variable {"+name+"} was not found in path.");
 		String p = null;
 		if (name.equals("/*"))
 			p = path.replaceAll("\\/\\*$", "/" + value);
@@ -1174,9 +1171,8 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 *
 	 * @param pair The parameter.
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
-	public RestRequest query(NameValuePair pair) throws RestCallException {
+	public RestRequest query(NameValuePair pair) {
 		return queries(pair);
 	}
 
@@ -1240,9 +1236,8 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 * 		<li>Value converted to a string using the configured part serializer.
 	 * 	</ul>
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
-	public RestRequest query(AddFlag flag, String name, Object value) throws RestCallException {
+	public RestRequest query(AddFlag flag, String name, Object value) {
 		return queries(flag, serializedPart(name, value, QUERY, partSerializer, null, EnumSet.of(flag)));
 	}
 
@@ -1278,9 +1273,8 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 * 		<li>A collection or array of anything on this list.
 	 * </ul>
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
-	public RestRequest queries(Object...params) throws RestCallException {
+	public RestRequest queries(Object...params) {
 		return queries(APPEND, params);
 	}
 
@@ -1323,16 +1317,14 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 * 		<li>A collection or array of anything on this list.
 	 * </ul>
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
-	public RestRequest queries(AddFlag flag, Object...params) throws RestCallException {
+	public RestRequest queries(AddFlag flag, Object...params) {
 		List<NameValuePair> l = new ArrayList<>();
 		for (Object o : params) {
 			if (BasicPart.canCast(o)) {
 				l.add(BasicPart.cast(o));
 			} else if (o instanceof PartList) {
-				for (NameValuePair p : (PartList)o)
-					l.add(p);
+				((PartList)o).forEach(x -> l.add(x));
 			} else if (o instanceof Collection) {
 				for (Object o2 : (Collection<?>)o)
 					l.add(BasicPart.cast(o2));
@@ -1346,7 +1338,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 				for (Map.Entry<String,Object> e : toBeanMap(o).entrySet())
 					l.add(serializedPart(e.getKey(), e.getValue(), QUERY, partSerializer, null, EnumSet.of(flag)));
 			} else if (o != null) {
-				throw new RestCallException(null, null, "Invalid type passed to queries(): {0}", className(o));
+				throw new BasicRuntimeException("Invalid type passed to queries(): {0}", className(o));
 			}
 		}
 		return innerQuery(EnumSet.of(flag), l);
@@ -1435,8 +1427,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 		if (BasicPart.canCast(value)) {
 			l.add(BasicPart.cast(value));
 		} else if (value instanceof PartList) {
-			for (Object o : (PartList)value)
-				l.add(BasicPart.cast(o));
+			((PartList)value).forEach(x -> l.add(x));
 		} else if (value instanceof Collection) {
 			for (Object o : (Collection<?>)value)
 				l.add(BasicPart.cast(o));
@@ -1627,9 +1618,8 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 * 		<li>Value converted to a string using the configured part serializer.
 	 * 	</ul>
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
-	public RestRequest formData(AddFlag flag, String name, Object value) throws RestCallException {
+	public RestRequest formData(AddFlag flag, String name, Object value) {
 		return formDatas(flag, serializedPart(name, value, FORMDATA, partSerializer, null, EnumSet.of(flag)));
 	}
 
@@ -1710,16 +1700,14 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 	 * 		<li>A collection or array of anything on this list.
 	 * </ul>
 	 * @return This object (for method chaining).
-	 * @throws RestCallException Invalid input.
 	 */
-	public RestRequest formDatas(AddFlag flag, Object...params) throws RestCallException {
+	public RestRequest formDatas(AddFlag flag, Object...params) {
 		List<NameValuePair> l = new ArrayList<>();
 		for (Object o : params) {
 			if (BasicPart.canCast(o)) {
 				l.add(BasicPart.cast(o));
 			} else if (o instanceof PartList) {
-				for (NameValuePair p : (PartList)o)
-					l.add(p);
+				((PartList)o).forEach(x -> l.add(x));
 			} else if (o instanceof Collection) {
 				for (Object o2 : (Collection<?>)o)
 					l.add(BasicPart.cast(o2));
@@ -1733,7 +1721,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 				for (Map.Entry<String,Object> e : toBeanMap(o).entrySet())
 					l.add(serializedPart(e.getKey(), e.getValue(), FORMDATA, partSerializer, null, EnumSet.of(flag)));
 			} else if (o != null) {
-				throw new RestCallException(null, null, "Invalid type passed to formDatas(): {0}", className(o));
+				throw new BasicRuntimeException("Invalid type passed to formDatas(): {0}", className(o));
 			}
 		}
 		return innerFormData(EnumSet.of(flag), l);
@@ -1831,8 +1819,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 		if (BasicPart.canCast(value)) {
 			l.add(BasicPart.cast(value));
 		} else if (value instanceof PartList) {
-			for (Part o : (PartList)value)
-				l.add(o);
+			((PartList)value).forEach(x -> l.add(x));
 		} else if (value instanceof Collection) {
 			for (Object o : (Collection<?>)value)
 				l.add(BasicPart.cast(o));
@@ -2955,7 +2942,7 @@ public class RestRequest extends BeanSession implements HttpUriRequest, Configur
 				if (formData != null)
 					entity = new UrlEncodedFormEntity(formData);
 				else if (input2 instanceof PartList)
-					entity = new UrlEncodedFormEntity((PartList)input2);
+					entity = new UrlEncodedFormEntity(((PartList)input2).asNameValuePairs());
 				else if (input2 instanceof HttpResource) {
 					HttpResource r = (HttpResource)input2;
 					r.getHeaders().forEach(x -> addHeader(x));
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOperationContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOperationContext.java
index aa17db6..e7ec3d0 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOperationContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOperationContext.java
@@ -1457,7 +1457,7 @@ public class RestOperationContext extends BeanContext implements Comparable<Rest
 
 		PartListBuilder x = PartList.create();
 
-		x.appendUnique(properties.getInstanceArray(RESTOP_defaultQuery, Part.class, beanStore).orElse(new Part[0]));
+		x.setDefault(properties.getInstanceArray(RESTOP_defaultQuery, Part.class, beanStore).orElse(new Part[0]));
 
 		for (Annotation[] aa : method.getParameterAnnotations()) {
 			for (Annotation a : aa) {
@@ -1466,7 +1466,7 @@ public class RestOperationContext extends BeanContext implements Comparable<Rest
 					String def = joinnlFirstNonEmptyArray(h._default(), h.df());
 					if (def != null) {
 						try {
-							x.appendUnique(basicPart(firstNonEmpty(h.name(), h.n(), h.value()), parseAnything(def)));
+							x.setDefault(basicPart(firstNonEmpty(h.name(), h.n(), h.value()), parseAnything(def)));
 						} catch (ParseException e) {
 							throw new ConfigException(e, "Malformed @Query annotation");
 						}
@@ -1501,7 +1501,7 @@ public class RestOperationContext extends BeanContext implements Comparable<Rest
 
 		PartListBuilder x = PartList.create();
 
-		x.appendUnique(properties.getInstanceArray(RESTOP_defaultFormData, Part.class, beanStore).orElse(new Part[0]));
+		x.setDefault(properties.getInstanceArray(RESTOP_defaultFormData, Part.class, beanStore).orElse(new Part[0]));
 
 		for (Annotation[] aa : method.getParameterAnnotations()) {
 			for (Annotation a : aa) {
@@ -1510,7 +1510,7 @@ public class RestOperationContext extends BeanContext implements Comparable<Rest
 					String def = joinnlFirstNonEmptyArray(h._default(), h.df());
 					if (def != null) {
 						try {
-							x.appendUnique(basicPart(firstNonEmpty(h.name(), h.n(), h.value()), parseAnything(def)));
+							x.setDefault(basicPart(firstNonEmpty(h.name(), h.n(), h.value()), parseAnything(def)));
 						} catch (ParseException e) {
 							throw new ConfigException(e, "Malformed @FormData annotation");
 						}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/PartList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/PartList_Test.java
deleted file mode 100644
index 6de157c..0000000
--- a/juneau-utest/src/test/java/org/apache/juneau/http/PartList_Test.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// ***************************************************************************************************************************
-// * 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 static org.apache.juneau.assertions.Assertions.*;
-import static org.junit.runners.MethodSorters.*;
-import static org.apache.juneau.http.HttpParts.*;
-
-import org.apache.juneau.collections.*;
-import org.apache.juneau.http.part.*;
-import org.apache.juneau.httppart.*;
-import org.apache.juneau.oapi.*;
-import org.junit.*;
-
-@FixMethodOrder(NAME_ASCENDING)
-public class PartList_Test {
-
-	@Test
-	public void a01_basic() {
-		PartListBuilder x = PartList.create();
-
-		assertObject(x.build().iterator()).asJson().is("[]");
-		x.add(part("Foo","bar"));
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar']");
-		x.add(part("Foo","baz"));
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar','Foo=baz']");
-		x.add(PartList.of());
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar','Foo=baz']");
-		x.add(PartList.of(part("Foo","qux")));
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar','Foo=baz','Foo=qux']");
-		x.add(PartList.of(part("Foo","q2x"), part("Foo","q3x")));
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar','Foo=baz','Foo=qux','Foo=q2x','Foo=q3x']");
-		x.add(PartList.of(part("Foo","q4x"),part("Foo","q5x")));
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar','Foo=baz','Foo=qux','Foo=q2x','Foo=q3x','Foo=q4x','Foo=q5x']");
-		x.add((PartList)null);
-		assertObject(x.build().iterator()).asJson().is("['Foo=bar','Foo=baz','Foo=qux','Foo=q2x','Foo=q3x','Foo=q4x','Foo=q5x']");
-
-		assertObject(new PartList.Null().iterator()).asJson().is("[]");
-	}
-
-	@Test
-	public void a02_creators() {
-		PartList x;
-
-		x = partList(part("Foo","bar"), part("Foo","baz"), null);
-		assertObject(x.iterator()).asJson().is("['Foo=bar','Foo=baz']");
-
-		x = partList(AList.of(part("Foo","bar"), part("Foo","baz"), null));
-		assertObject(x.iterator()).asJson().is("['Foo=bar','Foo=baz']");
-
-		x = partList("Foo","bar","Foo","baz");
-		assertObject(x.iterator()).asJson().is("['Foo=bar','Foo=baz']");
-
-		assertThrown(()->partList("Foo")).is("Odd number of parameters passed into PartList.ofPairs()");
-	}
-
-	@Test
-	public void a03_addMethods() {
-		String pname = "NameValuePairSupplierTest.x";
-
-		PartListBuilder x = PartList.create().resolving();
-		System.setProperty(pname, "y");
-
-		x.add("X1","bar");
-		x.add("X2","$S{"+pname+"}");
-		x.add("X3","bar");
-		x.add("X4",()->"$S{"+pname+"}");
-		x.add("X5","bar",HttpPartType.QUERY,openApiSession(),null,false);
-		x.add("X6","$S{"+pname+"}",HttpPartType.QUERY,openApiSession(),null,false);
-
-		assertString(x.build().toString()).is("X1=bar&X2=y&X3=bar&X4=y&X5=bar&X6=y");
-
-		System.setProperty(pname, "z");
-
-		assertString(x.build().toString()).is("X1=bar&X2=z&X3=bar&X4=z&X5=bar&X6=z");
-
-		System.clearProperty(pname);
-	}
-
-	//-----------------------------------------------------------------------------------------------------------------
-	// Utility methods
-	//-----------------------------------------------------------------------------------------------------------------
-
-	private static Part part(String name, Object val) {
-		return basicPart(name, val);
-	}
-
-	private static HttpPartSerializerSession openApiSession() {
-		return OpenApiSerializer.DEFAULT.createPartSession(null);
-	}
-}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/header/Accept_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/header/Accept_Test.java
index 5069da7..2f39874 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/header/Accept_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/header/Accept_Test.java
@@ -27,6 +27,9 @@ import org.apache.juneau.rest.client.*;
 import org.apache.juneau.rest.mock.*;
 import org.junit.*;
 
+/**
+ * Tests: {@link Accept}
+ */
 @FixMethodOrder(NAME_ASCENDING)
 public class Accept_Test {
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java
index fa69bd8..26d6633 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java
@@ -95,7 +95,7 @@ public class HeaderBeanMeta_Test {
 
 		HeaderBeanMeta<A5> a5 = HeaderBeanMeta.of(A5.class);
 		assertThrown(()->a5.construct("foo")).contains("Constructor for type "+TNAME+"$A5 could not be found.");
-		assertString(a4.getSchema().getName()).is("A4");
+		assertString(a5.getSchema().getName()).is("A5");
 
 		HeaderBeanMeta<A6> a6 = HeaderBeanMeta.of(A6.class);
 		assertThrown(()->a6.construct("X", "foo")).contains("oops");
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderList_Test.java
index bc64ad0..733c189 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderList_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderList_Test.java
@@ -25,6 +25,9 @@ import org.apache.juneau.httppart.*;
 import org.apache.juneau.oapi.*;
 import org.junit.*;
 
+/**
+ * Tests: {@link HeaderList}, {@link HeaderListBuilder}, {@link BasicHeaderIterator}
+ */
 @FixMethodOrder(NAME_ASCENDING)
 public class HeaderList_Test {
 
@@ -377,6 +380,7 @@ public class HeaderList_Test {
 			.set(FOO_3)
 			.set(BAR_1)
 			.set((Header)null)
+			.set((HeaderList)null)
 			.build();
 		assertObject(x).isString("[Foo: 3, Bar: 1]");
 
@@ -434,6 +438,99 @@ public class HeaderList_Test {
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
+	// BasicHeaderIterator
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void c01_iterators() {
+		HeaderList x = HeaderList.of(Accept.TEXT_XML,ContentType.TEXT_XML);
+
+		HeaderIterator i1 = x.iterator();
+		assertObject(i1.nextHeader()).isString("Accept: text/xml");
+		assertObject(i1.nextHeader()).isString("Content-Type: text/xml");
+		assertThrown(()->i1.nextHeader()).contains("Iteration already finished.");
+
+		HeaderIterator i2 = x.iterator();
+		assertObject(i2.next()).isString("Accept: text/xml");
+		assertObject(i2.nextHeader()).isString("Content-Type: text/xml");
+		assertThrown(()->i2.next()).contains("Iteration already finished.");
+
+		HeaderIterator i3 = x.iterator("accept");
+		assertObject(i3.nextHeader()).isString("Accept: text/xml");
+		assertThrown(()->i3.nextHeader()).contains("Iteration already finished.");
+
+		HeaderList x2 = HeaderList.create().append(Accept.TEXT_XML,ContentType.TEXT_XML).caseSensitive().build();
+
+		HeaderIterator i4 = x2.iterator("Accept");
+		assertObject(i4.nextHeader()).isString("Accept: text/xml");
+		assertThrown(()->i4.nextHeader()).contains("Iteration already finished.");
+
+		HeaderIterator i5 = x2.iterator("accept");
+		assertThrown(()->i5.nextHeader()).contains("Iteration already finished.");
+
+		assertThrown(()->i5.remove()).contains("Remove is not supported.");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Default headers
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void d01_defaultHeaders() {
+		HeaderList x1 = HeaderList.create().setDefault(Accept.TEXT_XML).build();
+		assertObject(x1).isString("[Accept: text/xml]");
+
+		HeaderList x2 = HeaderList.create().set(Accept.TEXT_PLAIN).setDefault(Accept.TEXT_XML).build();
+		assertObject(x2).isString("[Accept: text/plain]");
+
+		HeaderList x3 = HeaderList.create().set(ContentType.TEXT_XML,Accept.TEXT_PLAIN,ContentType.TEXT_XML).setDefault(Accept.TEXT_XML).build();
+		assertObject(x3).isString("[Content-Type: text/xml, Accept: text/plain, Content-Type: text/xml]");
+
+		HeaderList x4 = HeaderList.create().set(ContentType.TEXT_XML,ContentType.TEXT_XML).setDefault(Accept.TEXT_XML).build();
+		assertObject(x4).isString("[Content-Type: text/xml, Content-Type: text/xml, Accept: text/xml]");
+
+		HeaderList x5 = HeaderList.create().set(ContentType.TEXT_XML,ContentType.TEXT_XML).setDefault(Accept.TEXT_XML).setDefault(ContentType.TEXT_HTML).build();
+		assertObject(x5).isString("[Content-Type: text/xml, Content-Type: text/xml, Accept: text/xml]");
+
+		HeaderList x6 = HeaderList.create().setDefault(Accept.TEXT_XML,Accept.TEXT_PLAIN).build();
+		assertObject(x6).isString("[Accept: text/xml, Accept: text/plain]");
+
+		HeaderList x7 = HeaderList.create().setDefault(Accept.TEXT_XML).setDefault(Accept.TEXT_PLAIN).build();
+		assertObject(x7).isString("[Accept: text/plain]");
+
+		HeaderList x8 = HeaderList.create().setDefault(Accept.TEXT_XML,Accept.TEXT_HTML).setDefault(Accept.TEXT_PLAIN).build();
+		assertObject(x8).isString("[Accept: text/plain]");
+
+		HeaderList x9 = HeaderList
+			.create()
+			.setDefault((Header)null)
+			.setDefault((HeaderList)null)
+			.setDefault((Header[])null)
+			.setDefault((List<Header>)null)
+			.build();
+		assertObject(x9).isString("[]");
+
+		HeaderList x10 = HeaderList.create().setDefault("Accept","text/xml").build();
+		assertObject(x10).isString("[Accept: text/xml]");
+
+		HeaderList x11 = HeaderList.create().setDefault("Accept",()->"text/xml").build();
+		assertObject(x11).isString("[Accept: text/xml]");
+
+		HeaderList x12 = HeaderList.create().set(ContentType.TEXT_XML,ContentType.TEXT_PLAIN).setDefault(AList.of(Accept.TEXT_XML,ContentType.TEXT_HTML,null)).build();
+		assertObject(x12).isString("[Content-Type: text/xml, Content-Type: text/plain, Accept: text/xml]");
+
+		HeaderList x13 = HeaderList.create().set(ContentType.TEXT_XML,ContentType.TEXT_PLAIN).setDefault(HeaderList.of(Accept.TEXT_XML,ContentType.TEXT_HTML,null)).build();
+		assertObject(x13).isString("[Content-Type: text/xml, Content-Type: text/plain, Accept: text/xml]");
+
+		HeaderList x14 = HeaderList.create().set(ContentType.TEXT_XML,ContentType.TEXT_PLAIN)
+			.setDefault(AList.of(Accept.TEXT_XML,ContentType.TEXT_HTML,null))
+			.setDefault(AList.of(Accept.TEXT_HTML,ContentType.TEXT_XML,null))
+			.setDefault(AList.of(Age.of(1)))
+			.build();
+		assertObject(x14).isString("[Content-Type: text/xml, Content-Type: text/plain, Accept: text/html, Age: 1]");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
 	// Utility methods
 	//-----------------------------------------------------------------------------------------------------------------
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/part/PartBeanMeta_Test.java
similarity index 79%
copy from juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java
copy to juneau-utest/src/test/java/org/apache/juneau/http/part/PartBeanMeta_Test.java
index fa69bd8..1b0c8b8 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/header/HeaderBeanMeta_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/part/PartBeanMeta_Test.java
@@ -10,7 +10,7 @@
 // * "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.header;
+package org.apache.juneau.http.part;
 
 import static org.junit.Assert.*;
 import static org.junit.runners.MethodSorters.*;
@@ -20,10 +20,13 @@ import static org.apache.juneau.internal.StringUtils.*;
 import org.apache.juneau.http.annotation.*;
 import org.junit.*;
 
+/**
+ * Tests: {@link PartBeanMeta}
+ */
 @FixMethodOrder(NAME_ASCENDING)
-public class HeaderBeanMeta_Test {
+public class PartBeanMeta_Test {
 
-	private static final String TNAME = HeaderBeanMeta_Test.class.getName();
+	private static final String TNAME = PartBeanMeta_Test.class.getName();
 
 	public static class A1 {
 		public String name, value;
@@ -43,7 +46,7 @@ public class HeaderBeanMeta_Test {
 		}
 	}
 
-	@Header(name="A3")
+	@Query(name="A3")
 	public static class A3 {
 		public String value;
 
@@ -52,7 +55,7 @@ public class HeaderBeanMeta_Test {
 		}
 	}
 
-	@Header("A4")
+	@FormData("A4")
 	public static class A4 {
 		public String value;
 
@@ -61,7 +64,7 @@ public class HeaderBeanMeta_Test {
 		}
 	}
 
-	@Header(name="A5")
+	@Path(name="A5")
 	public static class A5 {}
 
 	public static class A6 {
@@ -70,34 +73,41 @@ public class HeaderBeanMeta_Test {
 		}
 	}
 
+	@ResponseHeader(name="A7")
+	public static class A7 {}
+
 	@Test
 	public void a01_basic() {
-		HeaderBeanMeta<A1> a1 = HeaderBeanMeta.of(A1.class);
-		assertTrue(a1 == HeaderBeanMeta.of(A1.class));
+		PartBeanMeta<A1> a1 = PartBeanMeta.of(A1.class);
+		assertTrue(a1 == PartBeanMeta.of(A1.class));
 		assertObject(a1.construct("X", "foo")).isJson("{name:'X',value:'foo'}");
 		assertThrown(()->a1.construct("foo")).contains("Constructor for type "+TNAME+"$A1 requires a name as the first argument.");
 		assertString(a1.getSchema().getName()).isNull();
 
-		HeaderBeanMeta<A2> a2 = HeaderBeanMeta.of(A2.class);
+		PartBeanMeta<A2> a2 = PartBeanMeta.of(A2.class);
 		assertObject(a2.construct("X", "foo")).isJson("{name:'X',value:'foo'}");
 		assertThrown(()->a2.construct("foo")).contains("Constructor for type "+TNAME+"$A2 requires a name as the first argument.");
 		assertString(a2.getSchema().getName()).isNull();
 
-		HeaderBeanMeta<A3> a3 = HeaderBeanMeta.of(A3.class);
+		PartBeanMeta<A3> a3 = PartBeanMeta.of(A3.class);
 		assertObject(a3.construct("X", "foo")).isJson("{value:'foo'}");
 		assertObject(a3.construct("foo")).isJson("{value:'foo'}");
 		assertString(a3.getSchema().getName()).is("A3");
 
-		HeaderBeanMeta<A4> a4 = HeaderBeanMeta.of(A4.class);
+		PartBeanMeta<A4> a4 = PartBeanMeta.of(A4.class);
 		assertObject(a4.construct("X", "foo")).isJson("{value:'foo'}");
 		assertObject(a4.construct("foo")).isJson("{value:'foo'}");
 		assertString(a4.getSchema().getName()).is("A4");
 
-		HeaderBeanMeta<A5> a5 = HeaderBeanMeta.of(A5.class);
+		PartBeanMeta<A5> a5 = PartBeanMeta.of(A5.class);
 		assertThrown(()->a5.construct("foo")).contains("Constructor for type "+TNAME+"$A5 could not be found.");
-		assertString(a4.getSchema().getName()).is("A4");
+		assertString(a5.getSchema().getName()).is("A5");
 
-		HeaderBeanMeta<A6> a6 = HeaderBeanMeta.of(A6.class);
+		PartBeanMeta<A6> a6 = PartBeanMeta.of(A6.class);
 		assertThrown(()->a6.construct("X", "foo")).contains("oops");
+
+		PartBeanMeta<A7> a7 = PartBeanMeta.of(A7.class);
+		assertThrown(()->a7.construct("foo")).contains("Constructor for type "+TNAME+"$A7 could not be found.");
+		assertString(a7.getSchema().getName()).is("A7");
 	}
 }
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/part/PartList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/part/PartList_Test.java
new file mode 100644
index 0000000..6c8128c
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/part/PartList_Test.java
@@ -0,0 +1,580 @@
+// ***************************************************************************************************************************
+// * 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.part;
+
+import static org.apache.juneau.assertions.Assertions.*;
+import static org.apache.juneau.http.HttpParts.*;
+import static org.junit.runners.MethodSorters.*;
+
+import java.util.*;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.oapi.*;
+import org.junit.*;
+
+/**
+ * Tests: {@link PartList}, {@link PartListBuilder}, {@link BasicPartIterator}
+ */
+@FixMethodOrder(NAME_ASCENDING)
+public class PartList_Test {
+
+	private static final Part
+		FOO_1 = part("Foo","1"),
+		FOO_2 = part("Foo","2"),
+		FOO_3 = part("Foo","3"),
+		FOO_4 = part("Foo","4"),
+		FOO_5 = part("Foo","5"),
+		FOO_6 = part("Foo","6"),
+		FOO_7 = part("Foo","7"),
+		BAR_1 = part("Bar","1"),
+		BAR_2 = part("Bar","2"),
+
+		X_x = part("X", "x");
+
+	@Query("a")
+	public static class APart extends BasicStringPart {
+		public static APart X = new APart("x"), Y = new APart("y"), Z = new APart("z");
+		public APart(Object value) {
+			super("a", value);
+		}
+	}
+
+	@FormData("b")
+	public static class BPart extends BasicStringPart {
+		public static BPart X = new BPart("x"), Y = new BPart("y"), Z = new BPart("z");
+		public BPart(Object value) {
+			super("b", value);
+		}
+	}
+
+	@Path("c")
+	public static class CPart extends BasicStringPart {
+		public static CPart X = new CPart("x");
+		public CPart(Object value) {
+			super("c", value);
+		}
+	}
+
+	@Test
+	public void a01_basic() {
+		PartListBuilder x = PartList.create();
+
+		assertObject(x.build()).isString("");
+		x.append(FOO_1);
+		assertObject(x.build()).isString("Foo=1");
+		x.append(FOO_2);
+		assertObject(x.build()).isString("Foo=1&Foo=2");
+		x.append(PartList.of().getAll());
+		assertObject(x.build()).isString("Foo=1&Foo=2");
+		x.append(PartList.of(FOO_3).getAll());
+		assertObject(x.build()).isString("Foo=1&Foo=2&Foo=3");
+		x.append(PartList.of(FOO_4, FOO_5).getAll());
+		assertObject(x.build()).isString("Foo=1&Foo=2&Foo=3&Foo=4&Foo=5");
+		x.append(PartList.of(FOO_6, FOO_7).getAll());
+		assertObject(x.build()).isString("Foo=1&Foo=2&Foo=3&Foo=4&Foo=5&Foo=6&Foo=7");
+		x.append((Part)null);
+		assertObject(x.build()).isString("Foo=1&Foo=2&Foo=3&Foo=4&Foo=5&Foo=6&Foo=7");
+		x.append((List<Part>)null);
+		assertObject(x.build()).isString("Foo=1&Foo=2&Foo=3&Foo=4&Foo=5&Foo=6&Foo=7");
+
+		assertObject(new PartList.Null()).isString("");
+	}
+
+	@Test
+	public void a02_creators() {
+		PartList x;
+
+		x = partList(FOO_1, FOO_2, null);
+		assertObject(x).isString("Foo=1&Foo=2");
+
+		x = partList(AList.of(FOO_1, FOO_2, null));
+		assertObject(x).isString("Foo=1&Foo=2");
+
+		x = partList("Foo","1","Foo","2");
+		assertObject(x).isString("Foo=1&Foo=2");
+
+		assertThrown(()->partList("Foo")).is("Odd number of parameters passed into PartList.ofPairs()");
+
+		x = PartList.of((List<Part>)null);
+		assertObject(x).isString("");
+
+		x = PartList.of(Collections.emptyList());
+		assertObject(x).isString("");
+
+		x = PartList.of(AList.of(FOO_1));
+		assertObject(x).isString("Foo=1");
+
+		x = PartList.of((Part[])null);
+		assertObject(x).isString("");
+
+		x = PartList.of();
+		assertObject(x).isString("");
+
+		x = PartList.of(FOO_1);
+		assertObject(x).isString("Foo=1");
+
+		x = PartList.ofPairs((Object[])null);
+		assertObject(x).isString("");
+
+		x = PartList.ofPairs();
+		assertObject(x).isString("");
+	}
+
+	@Test
+	public void a03_addMethods() {
+		String pname = "PartSupplierTest.x";
+
+		PartListBuilder x = PartList.create().resolving();
+		System.setProperty(pname, "y");
+
+		x.append("X1","bar");
+		x.append("X2","$S{"+pname+"}");
+		x.append("X3","bar");
+		x.append("X4",()->"$S{"+pname+"}");
+		x.append(new SerializedPart("X5","bar",HttpPartType.QUERY,openApiSession(),null,false));
+
+		assertObject(x.build()).isString("X1=bar&X2=y&X3=bar&X4=y&X5=bar");
+
+		System.setProperty(pname, "z");
+
+		assertObject(x.build()).isString("X1=bar&X2=z&X3=bar&X4=z&X5=bar");
+
+		System.clearProperty(pname);
+	}
+
+	@Test
+	public void a04_toArrayMethods() {
+		PartListBuilder x = PartList
+			.create()
+			.append("X1","1")
+			.append(partList("X2","2").getAll());
+		assertObject(x.build()).isString("X1=1&X2=2");
+	}
+
+	@Test
+	public void a05_copy() {
+		PartList x = PartList.of(FOO_1).copy().build();
+		assertObject(x).isString("Foo=1");
+	}
+
+	@Test
+	public void a06_getCondensed() {
+		PartList x = PartList.of(FOO_1);
+		assertObject(x.get((String)null)).isNull();
+		assertObject(x.get("Foo")).isString("Foo=1");
+		assertObject(x.get("Bar")).isNull();
+		x = PartList.of(FOO_1, FOO_2, FOO_3, X_x);
+		assertObject(x.get("Foo")).isString("Foo=1%2C2%2C3");
+		assertObject(x.get("Bar")).isNull();
+	}
+
+	@Query("Foo")
+	static class Foo extends BasicStringPart {
+		public Foo(Object value) {
+			super("Foo", value);
+		}
+	}
+
+	@Test
+	public void a07_getCondensed_asType() {
+		PartList x = PartList.of(FOO_1);
+		assertObject(x.get(null, APart.class)).isNull();
+		assertObject(x.get("Foo", APart.class)).isString("a=1");
+		assertObject(x.get("Bar", APart.class)).isNull();
+		x = PartList.of(FOO_1, FOO_2, FOO_3, X_x);
+		assertObject(x.get("Foo", APart.class)).isString("a=1%2C2%2C3");
+		assertObject(x.get("Bar", APart.class)).isNull();
+		assertObject(x.get(Foo.class)).isString("Foo=1%2C2%2C3");
+		final PartList x2 = x;
+		assertThrown(()->x2.get(String.class)).contains("Part name could not be found on bean type 'java.lang.String'");
+	}
+
+	@Test
+	public void a08_get() {
+		PartList x = PartList.of(FOO_1, FOO_2, X_x);
+		assertArray(x.getAll(null)).isString("[]");
+		assertArray(x.getAll("Foo")).isString("[Foo=1, Foo=2]");
+		assertArray(x.getAll("FOO")).isString("[]");
+		assertArray(x.getAll("Bar")).isString("[]");
+	}
+
+	@Test
+	public void a09_getFirst() {
+		PartList x = PartList.of(FOO_1, FOO_2, X_x);
+		assertObject(x.getFirst(null)).isNull();
+		assertObject(x.getFirst("Foo")).isString("Foo=1");
+		assertObject(x.getFirst("FOO")).isNull();
+		assertObject(x.getFirst("Bar")).isNull();
+	}
+
+	@Test
+	public void a10_getLast() {
+		PartList x = PartList.of(FOO_1, FOO_2, X_x);
+		assertObject(x.getLast(null)).isNull();
+		assertObject(x.getLast("Foo")).isString("Foo=2");
+		assertObject(x.getLast("FOO")).isNull();
+		assertObject(x.getLast("Bar")).isNull();
+	}
+
+	@Test
+	public void a11_contains() {
+		PartList x = PartList.of(FOO_1, FOO_2, X_x);
+		assertBoolean(x.contains(null)).isFalse();
+		assertBoolean(x.contains("Foo")).isTrue();
+		assertBoolean(x.contains("FOO")).isFalse();
+		assertBoolean(x.contains("Bar")).isFalse();
+	}
+
+	@Test
+	public void a12_headerIterator_all() {
+		PartList x = PartList.of();
+		assertBoolean(x.iterator().hasNext()).isFalse();
+		x = PartList.of(FOO_1);
+		assertBoolean(x.iterator().hasNext()).isTrue();
+	}
+
+	@Test
+	public void a13_headerIterator_single() {
+		PartList x = PartList.of();
+		assertBoolean(x.iterator("Foo").hasNext()).isFalse();
+		x = PartList.of(FOO_1);
+		assertBoolean(x.iterator("Foo").hasNext()).isTrue();
+		assertBoolean(x.iterator("FOO").hasNext()).isFalse();
+	}
+
+	@Test
+	public void a14_forEach_all() {
+		PartList x = PartList.of();
+
+		final AtomicInteger i1 = new AtomicInteger();
+		x.forEach(h -> i1.incrementAndGet());
+		assertInteger(i1.get()).is(0);
+
+		x = PartList.of(FOO_1, FOO_2);
+		final AtomicInteger i2 = new AtomicInteger();
+		x.forEach(h -> i2.incrementAndGet());
+		assertInteger(i2.get()).is(2);
+	}
+
+	@Test
+	public void a15_forEach_single() {
+		PartList x = PartList.of();
+
+		final AtomicInteger i1 = new AtomicInteger();
+		x.forEach("Foo", h -> i1.incrementAndGet());
+		assertInteger(i1.get()).is(0);
+
+		x = PartList.of(FOO_1, FOO_2, X_x);
+		final AtomicInteger i2 = new AtomicInteger();
+		x.forEach("Foo", h -> i2.incrementAndGet());
+		assertInteger(i2.get()).is(2);
+	}
+
+	@Test
+	public void a16_stream_all() {
+		PartList x = PartList.of();
+
+		final AtomicInteger i1 = new AtomicInteger();
+		x.stream().forEach(h -> i1.incrementAndGet());
+		assertInteger(i1.get()).is(0);
+
+		x = PartList.of(FOO_1, FOO_2);
+		final AtomicInteger i2 = new AtomicInteger();
+		x.stream().forEach(h -> i2.incrementAndGet());
+		assertInteger(i2.get()).is(2);
+	}
+
+	@Test
+	public void a17_stream_single() {
+		PartList x = PartList.of();
+
+		final AtomicInteger i1 = new AtomicInteger();
+		x.stream("Foo").forEach(h -> i1.incrementAndGet());
+		assertInteger(i1.get()).is(0);
+
+		x = PartList.of(FOO_1, FOO_2, X_x);
+		final AtomicInteger i2 = new AtomicInteger();
+		x.stream("Foo").forEach(h -> i2.incrementAndGet());
+		assertInteger(i2.get()).is(2);
+	}
+
+	@Test
+	public void a18_caseSensitive() {
+		PartList x1 = PartList.create().append(FOO_1, FOO_2, X_x).build();
+		assertArray(x1.getAll("Foo")).isString("[Foo=1, Foo=2]");
+		assertArray(x1.getAll("FOO")).isString("[]");
+
+		PartList x2 = x1.copy().caseInsensitive().build();
+		assertArray(x2.getAll("Foo")).isString("[Foo=1, Foo=2]");
+		assertArray(x2.getAll("FOO")).isString("[Foo=1, Foo=2]");
+	}
+
+	@Test
+	public void a19_size() {
+		PartList x = PartList.of(FOO_1);
+		assertInteger(x.size()).is(1);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Builder methods
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void b01_builder_clear() {
+		PartListBuilder x = PartList.create();
+		x.append(FOO_1);
+		x.clear();
+		assertObject(x.build()).isString("");
+	}
+
+	@Test
+	public void b02_builder_append() {
+		PartList x1 = PartList.create().append(FOO_1).build();
+		PartList x2 = PartList
+			.create()
+			.append()
+			.append((PartList)null)
+			.append((Part)null)
+			.append((Part[])null)
+			.append(x1)
+			.append(FOO_2, FOO_3)
+			.append("Bar", "b1")
+			.append("Bar", ()->"b2")
+			.append((List<Part>)null)
+			.append(AList.of(FOO_4))
+			.build();
+		assertObject(x2).isString("Foo=1&Foo=2&Foo=3&Bar=b1&Bar=b2&Foo=4");
+	}
+
+	@Test
+	public void b03_builder_prepend() {
+		PartList x1 = PartList.create().append(FOO_1).build();
+		PartList x2 = PartList
+			.create()
+			.prepend()
+			.prepend((PartList)null)
+			.prepend((Part)null)
+			.prepend((Part[])null)
+			.prepend(x1)
+			.prepend(FOO_2, FOO_3)
+			.prepend("Bar", "b1")
+			.prepend("Bar", ()->"b2")
+			.prepend((List<Part>)null)
+			.prepend(AList.of(FOO_4))
+			.build();
+		assertObject(x2).isString("Foo=4&Bar=b2&Bar=b1&Foo=2&Foo=3&Foo=1");
+	}
+
+	@Test
+	public void b04_builder_remove() {
+		PartList x = PartList
+			.create()
+			.append(FOO_1,FOO_2,FOO_3,FOO_4,FOO_5,FOO_6,FOO_7)
+			.remove((PartList)null)
+			.remove((Part)null)
+			.remove(PartList.of(FOO_1))
+			.remove(FOO_2)
+			.remove(FOO_3, FOO_4)
+			.remove(AList.of(FOO_5))
+			.build();
+		assertObject(x).isString("Foo=6&Foo=7");
+
+		x = PartList.create().append(FOO_1,FOO_2).remove((String[])null).remove("Bar","Foo").build();
+		assertObject(x).isString("");
+	}
+
+	@Test
+	public void b05_builder_set() {
+		PartList x = null;
+
+		x = PartList
+			.create()
+			.append(FOO_1,FOO_2)
+			.set(FOO_3)
+			.set(BAR_1)
+			.set((Part)null)
+			.set((PartList)null)
+			.build();
+		assertObject(x).isString("Foo=3&Bar=1");
+
+		x = PartList
+			.create()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set(FOO_3)
+			.build();
+		assertObject(x).isString("Bar=1&Foo=3&Bar=2");
+
+		x = PartList
+			.create()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set((Part[])null)
+			.set(null,FOO_3,FOO_4,FOO_5)
+			.build();
+		assertObject(x).isString("Bar=1&Bar=2&Foo=3&Foo=4&Foo=5");
+
+		x = PartList
+			.create()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set((List<Part>)null)
+			.set(AList.of(null,FOO_3,FOO_4,FOO_5))
+			.build();
+		assertObject(x).isString("Bar=1&Bar=2&Foo=3&Foo=4&Foo=5");
+
+		x = PartList
+			.create()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set("FOO", "x")
+			.build();
+		assertObject(x).isString("Bar=1&FOO=x&Bar=2");
+
+		x = PartList
+			.create()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set("FOO", ()->"x")
+			.build();
+		assertObject(x).isString("Bar=1&FOO=x&Bar=2");
+
+		x = PartList
+			.create()
+			.caseInsensitive()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set("FOO", ()->"x")
+			.build();
+		assertObject(x).isString("Bar=1&Foo=1&Foo=2&Bar=2&FOO=x");
+
+		x = PartList
+			.create()
+			.append(BAR_1,FOO_1,FOO_2,BAR_2)
+			.set(PartList.of(FOO_3,FOO_4))
+			.build();
+		assertObject(x).isString("Bar=1&Bar=2&Foo=3&Foo=4");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// BasicPartIterator
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void c01_iterators() {
+		PartList x = PartList.of(APart.X, BPart.X);
+
+		PartIterator i1 = x.iterator();
+		assertObject(i1.next()).isString("a=x");
+		assertObject(i1.next()).isString("b=x");
+		assertThrown(()->i1.next()).contains("Iteration already finished.");
+
+		PartIterator i2 = x.iterator();
+		assertObject(i2.next()).isString("a=x");
+		assertObject(i2.next()).isString("b=x");
+		assertThrown(()->i2.next()).contains("Iteration already finished.");
+
+		PartIterator i3 = x.iterator("a");
+		assertObject(i3.next()).isString("a=x");
+		assertThrown(()->i3.next()).contains("Iteration already finished.");
+
+		PartIterator i4 = x.iterator("A");
+		assertThrown(()->i4.next()).contains("Iteration already finished.");
+
+		PartList x2 = PartList.create().append(APart.X,BPart.X).caseInsensitive().build();
+
+		PartIterator i5 = x2.iterator("A");
+		assertObject(i5.next()).isString("a=x");
+		assertThrown(()->i5.next()).contains("Iteration already finished.");
+
+		assertThrown(()->i5.remove()).contains("Remove is not supported.");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Default headers
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void d01_defaultParts() {
+		PartList x1 = PartList.create().setDefault(APart.X).build();
+		assertObject(x1).isString("a=x");
+
+		PartList x2 = PartList.create().set(APart.X).setDefault(APart.Y).build();
+		assertObject(x2).isString("a=x");
+
+		PartList x3 = PartList.create().set(BPart.X,APart.X,BPart.Y).setDefault(APart.Y).build();
+		assertObject(x3).isString("b=x&a=x&b=y");
+
+		PartList x4 = PartList.create().set(BPart.X,BPart.Y).setDefault(APart.X).build();
+		assertObject(x4).isString("b=x&b=y&a=x");
+
+		PartList x5 = PartList.create().set(BPart.X,BPart.Y).setDefault(APart.X).setDefault(BPart.X).build();
+		assertObject(x5).isString("b=x&b=y&a=x");
+
+		PartList x6 = PartList.create().setDefault(APart.X,APart.Y).build();
+		assertObject(x6).isString("a=x&a=y");
+
+		PartList x7 = PartList.create().setDefault(APart.X).setDefault(APart.Y).build();
+		assertObject(x7).isString("a=y");
+
+		PartList x8 = PartList.create().setDefault(APart.X,APart.Y).setDefault(APart.Z).build();
+		assertObject(x8).isString("a=z");
+
+		PartList x9 = PartList
+			.create()
+			.setDefault((Part)null)
+			.setDefault((PartList)null)
+			.setDefault((Part[])null)
+			.setDefault((List<Part>)null)
+			.build();
+		assertObject(x9).isString("");
+
+		PartList x10 = PartList.create().setDefault("a","x").build();
+		assertObject(x10).isString("a=x");
+
+		PartList x11 = PartList.create().setDefault("a",()->"x").build();
+		assertObject(x11).isString("a=x");
+
+		PartList x12 = PartList.create().set(BPart.X,BPart.Y).setDefault(AList.of(APart.X,BPart.Z,null)).build();
+		assertObject(x12).isString("b=x&b=y&a=x");
+
+		PartList x13 = PartList.create().set(BPart.X,BPart.Y).setDefault(PartList.of(APart.X,BPart.Z,null)).build();
+		assertObject(x13).isString("b=x&b=y&a=x");
+
+		PartList x14 = PartList.create().set(BPart.X,BPart.Y)
+			.setDefault(AList.of(APart.X,BPart.X,null))
+			.setDefault(AList.of(APart.Y,BPart.Y,null))
+			.setDefault(AList.of(CPart.X))
+			.build();
+		assertObject(x14).isString("b=x&b=y&a=y&c=x");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other tests
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void e01_asNameValuePairs() {
+		PartList x = PartList.of(APart.X);
+		assertObject(x.asNameValuePairs()).isString("[a=x]");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Utility methods
+	//-----------------------------------------------------------------------------------------------------------------
+
+	private static Part part(String name, Object val) {
+		return basicPart(name, val);
+	}
+
+	private static HttpPartSerializerSession openApiSession() {
+		return OpenApiSerializer.DEFAULT.createPartSession(null);
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
index d3ea94d..fa992e1 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_FormDataAnnotation_Test.java
@@ -848,11 +848,7 @@ public class Remote_FormDataAnnotation_Test {
 		}
 		@FormData
 		public NameValuePair[] getE() {
-			return parts("e1","v1","e2",123,"e3",null,"e4","").getAll().toArray(new NameValuePair[0]);
-		}
-		@FormData
-		public BasicPart[] getF() {
-			return parts("f1","v1","f2",123,"f3",null,"f4","").getAll().toArray(new BasicPart[0]);
+			return parts("e1","v1","e2",123,"e3",null,"e4","").getAll();
 		}
 	}
 
@@ -860,9 +856,9 @@ public class Remote_FormDataAnnotation_Test {
 	public void k03_requestBean_nameValuePairs() throws Exception {
 		K3 x1 = remote(K.class,K3.class);
 		K3 x2 = client(K.class).partSerializer(UonSerializer.class).build().getRemote(K3.class);
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:'',f1:'v1',f2:'123',f4:''}",x1.postX1(new K3a()));
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:'',f1:'v1',f2:'123',f4:''}",x2.postX1(new K3a()));
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:'',f1:'v1',f2:'123',f4:''}",x2.postX2(new K3a()));
+		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:''}",x1.postX1(new K3a()));
+		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:''}",x2.postX1(new K3a()));
+		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:''}",x2.postX2(new K3a()));
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_PathAnnotation_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_PathAnnotation_Test.java
index 993b945..6a9f831 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_PathAnnotation_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_PathAnnotation_Test.java
@@ -681,8 +681,8 @@ public class Remote_PathAnnotation_Test {
 
 	@Remote(path="/")
 	public static interface K3 {
-		@RemoteOp(path="/{a1}/{a2}/{a3}/{a4}/{b1}/{b2}/{b3}/{c1}/{c2}/{c3}/{c4}/{e1}/{e2}/{e3}/{e4}/{f1}/{f2}/{f3}/{f4}") String getX1(@Request K3a rb);
-		@RemoteOp(path="/{a1}/{a2}/{a3}/{a4}/{b1}/{b2}/{b3}/{c1}/{c2}/{c3}/{c4}/{e1}/{e2}/{e3}/{e4}/{f1}/{f2}/{f3}/{f4}") String getX2(@Request(serializer=XSerializer.class) K3a rb);
+		@RemoteOp(path="/{a1}/{a2}/{a3}/{a4}/{b1}/{b2}/{b3}/{c1}/{c2}/{c3}/{c4}/{e1}/{e2}/{e3}/{e4}") String getX1(@Request K3a rb);
+		@RemoteOp(path="/{a1}/{a2}/{a3}/{a4}/{b1}/{b2}/{b3}/{c1}/{c2}/{c3}/{c4}/{e1}/{e2}/{e3}/{e4}") String getX2(@Request(serializer=XSerializer.class) K3a rb);
 	}
 
 	public static class K3a {
@@ -704,11 +704,7 @@ public class Remote_PathAnnotation_Test {
 		}
 		@Path(aev=true)
 		public NameValuePair[] getE() {
-			return parts("e1","v1","e2",123,"e3",null,"e4","").getAll().toArray(new Part[0]);
-		}
-		@Path(aev=true)
-		public BasicPart[] getF() {
-			return parts("f1","v1","f2",123,"f3",null,"f4","").getAll().toArray(new BasicPart[0]);
+			return parts("e1","v1","e2",123,"e3",null,"e4","").getAll();
 		}
 	}
 
@@ -716,9 +712,9 @@ public class Remote_PathAnnotation_Test {
 	public void k03_requestBean_nameValuePairs() throws Exception {
 		K3 x1 = remote(K.class,K3.class);
 		K3 x2 = client(K.class).partSerializer(UonSerializer.class).build().getRemote(K3.class);
-		assertEquals("v1/123/null//true/123/null/v1/123/null//v1/123/null//v1/123/null/",x1.getX1(new K3a()));
-		assertEquals("v1/123/null//true/123/null/v1/123/null//v1/123/null//v1/123/null/",x2.getX1(new K3a()));
-		assertEquals("v1/123/null//true/123/null/v1/123/null//v1/123/null//v1/123/null/",x2.getX2(new K3a()));
+		assertEquals("v1/123/null//true/123/null/v1/123/null//v1/123/null/",x1.getX1(new K3a()));
+		assertEquals("v1/123/null//true/123/null/v1/123/null//v1/123/null/",x2.getX1(new K3a()));
+		assertEquals("v1/123/null//true/123/null/v1/123/null//v1/123/null/",x2.getX2(new K3a()));
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
index fed8c1b..f7fd2e5 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/http/remote/Remote_QueryAnnotation_Test.java
@@ -87,7 +87,6 @@ public class Remote_QueryAnnotation_Test {
 		@RemoteOp(path="a") String getX20(@Query PartList b);
 		@RemoteOp(path="a") String getX21(@Query NameValuePair b);
 		@RemoteOp(path="a") String getX22(@Query NameValuePair[] b);
-		@RemoteOp(path="a") String getX23(@Query BasicPart[] b);
 		@RemoteOp(path="a") String getX24(@Query String b);
 		@RemoteOp(path="a") String getX25(@Query List<NameValuePair> b);
 	}
@@ -116,8 +115,7 @@ public class Remote_QueryAnnotation_Test {
 		assertEquals("{foo:'bar'}",x.getX19(parts("foo","bar")));
 		assertEquals("{foo:'bar'}",x.getX20(parts("foo","bar")));
 		assertEquals("{foo:'bar'}",x.getX21(part("foo","bar")));
-		assertEquals("{foo:'bar'}",x.getX22(parts("foo","bar").getAll().toArray(new Part[0])));
-		assertEquals("{foo:'bar'}",x.getX23(parts("foo","bar").getAll().toArray(new BasicPart[0])));
+		assertEquals("{foo:'bar'}",x.getX22(parts("foo","bar").getAll()));
 		assertEquals("{foo:'bar'}",x.getX24("foo=bar"));
 		assertEquals("{}",x.getX24(null));
 		assertEquals("{foo:'bar'}",x.getX25(AList.of(part("foo","bar"))));
@@ -815,11 +813,7 @@ public class Remote_QueryAnnotation_Test {
 		}
 		@Query(aev=true)
 		public NameValuePair[] getE() {
-			return parts("e1","v1","e2",123,"e3",null,"e4","").getAll().toArray(new Part[0]);
-		}
-		@Query(aev=true)
-		public BasicPart[] getF() {
-			return parts("f1","v1","f2",123,"f3",null,"f4","").getAll().toArray(new BasicPart[0]);
+			return parts("e1","v1","e2",123,"e3",null,"e4","").getAll();
 		}
 	}
 
@@ -827,9 +821,8 @@ public class Remote_QueryAnnotation_Test {
 	public void k03_requestBean_nameValuePairs() throws Exception {
 		K3 x1 = remote(K.class,K3.class);
 		K3 x2 = client(K.class).partSerializer(UonSerializer.class).build().getRemote(K3.class);
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:'',f1:'v1',f2:'123',f4:''}",x1.getX1(new K3a()));
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:'',f1:'v1',f2:'123',f4:''}",x2.getX1(new K3a()));
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:'',f1:'v1',f2:'123',f4:''}",x2.getX2(new K3a()));
+		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:''}",x1.getX1(new K3a()));
+		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:'',e1:'v1',e2:'123',e4:''}",x2.getX1(new K3a()));
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_FormData_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_FormData_Test.java
index 1f81a33..0884200 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_FormData_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_FormData_Test.java
@@ -95,7 +95,6 @@ public class RestClient_FormData_Test {
 		client().formDatas(part("foo","bar"),null).build().post("/formData").run().assertBody().is("foo=bar");
 		client().build().post("/formData").formDatas(part("foo","bar"),null).run().assertBody().is("foo=bar");
 		client().formDatas(part("foo",null)).build().post("/formData").run().assertBody().is("");
-		client().formDatas(part(null,"foo")).build().post("/formData").run().assertBody().is("null=foo");
 		client().formDatas(part(null,null)).build().post("/formData").run().assertBody().is("");
 
 		client().build().post("/formData").formDatas(part("foo",null)).run().assertBody().is("");
diff --git a/juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java b/juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java
index 6ca5880..988f41b 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/utils/ArrayBuilder_Test.java
@@ -23,7 +23,7 @@ public class ArrayBuilder_Test {
 
 	@Test
 	public void a01_basic() {
-		ArrayBuilder<String> x = ArrayBuilder.create(String.class, 2);
+		ArrayBuilder<String> x = ArrayBuilder.create(String.class, 2, true);
 		assertObject(x.toArray()).asJson().is("[]");
 		x.add(null);
 		assertObject(x.toArray()).asJson().is("[]");