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 2022/08/12 21:35:15 UTC

[juneau] branch jbFixRestNpe updated: PartList should be an ArrayList.

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

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


The following commit(s) were added to refs/heads/jbFixRestNpe by this push:
     new c00f8fe89 PartList should be an ArrayList.
c00f8fe89 is described below

commit c00f8fe897dc13c40d57b49a5afc8a9f2d9f7f6d
Author: JamesBognar <ja...@salesforce.com>
AuthorDate: Fri Aug 12 17:34:49 2022 -0400

    PartList should be an ArrayList.
---
 .../juneau/collections/ControlledArrayList.java    | 391 +++++++++++++++++++++
 .../org/apache/juneau/rest/client/RestClient.java  |   2 +-
 .../java/org/apache/juneau/http/part/PartList.java | 267 ++++----------
 .../ArgsTest.java => collections/Args_Test.java}   |  11 +-
 .../collections/ControlledArrayList_Test.java      | 191 ++++++++++
 .../org/apache/juneau/http/part/PartList_Test.java |  12 +-
 6 files changed, 656 insertions(+), 218 deletions(-)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/ControlledArrayList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/ControlledArrayList.java
new file mode 100644
index 000000000..0b38c3613
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/ControlledArrayList.java
@@ -0,0 +1,391 @@
+// ***************************************************************************************************************************
+// * 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.collections;
+
+import java.util.*;
+import java.util.function.*;
+
+/**
+ * An array list that allows you to control whether it's read-only via a constructor parameter.
+ *
+ * <p>
+ * Override methods such as {@link #overrideAdd(int, Object)} are provided that bypass the unmodifiable restriction
+ * on the list.  They allow you to manipulate the list while not exposing the ability to manipulate the list through
+ * any of the methods provided by the {@link List} interface (meaning you can pass the object around as an unmodifiable List).
+ *
+ * @param <E> The element type.
+ */
+public class ControlledArrayList<E> extends ArrayList<E> {
+
+	private static final long serialVersionUID = -1L;
+
+	private final boolean modifiable;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param modifiable If <jk>true</jk>, this list can be modified through normal list operation methods on the {@link List} interface.
+	 */
+	public ControlledArrayList(boolean modifiable) {
+		this.modifiable = modifiable;
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param modifiable If <jk>true</jk>, this list can be modified through normal list operation methods on the {@link List} interface.
+	 * @param list The initial contents of this list.
+	 */
+	public ControlledArrayList(boolean modifiable, List<? extends E> list) {
+		super(list);
+		this.modifiable = modifiable;
+	}
+
+	void checkModifiable() {
+		if (! modifiable)
+			throw new UnsupportedOperationException("List is read-only.");
+	}
+
+	/**
+	 * Returns <jk>true</jk> if this list is modifiable.
+	 *
+	 * @return <jk>true</jk> if this list is modifiable.
+	 */
+	public boolean isModifiable() {
+		return modifiable;
+	}
+
+	@Override
+	public E set(int index, E element) {
+		checkModifiable();
+		return overrideSet(index, element);
+	}
+
+	/**
+	 * Same as {@link #set(int, Object)} but bypasses the modifiable flag.
+	 *
+	 * @param index Index of the element to replace.
+	 * @param element Element to be stored at the specified position.
+	 * @return The element previously at the specified position.
+	 */
+	public E overrideSet(int index, E element) {
+		return super.set(index, element);
+	}
+
+	@Override
+	public void add(int index, E element) {
+		checkModifiable();
+		overrideAdd(index, element);
+	}
+
+	/**
+	 * Same as {@link #add(int, Object)} but bypasses the modifiable flag.
+	 *
+	 * @param index Index of the element to replace.
+	 * @param element Element to be stored at the specified position.
+	 */
+	public void overrideAdd(int index, E element) {
+		super.add(index, element);
+	}
+
+	@Override
+	public E remove(int index) {
+		checkModifiable();
+		return overrideRemove(index);
+	}
+
+	/**
+	 * Same as {@link #remove(int)} but bypasses the modifiable flag.
+	 *
+	 * @param index Index of the element to remove.
+	 * @return The element that was removed from the list.
+	 */
+	public E overrideRemove(int index) {
+		return super.remove(index);
+	}
+
+	@Override
+	public boolean addAll(int index, Collection<? extends E> c) {
+		checkModifiable();
+		return overrideAddAll(index, c);
+	}
+
+	/**
+	 * Same as {@link #addAll(int,Collection)} but bypasses the modifiable flag.
+	 *
+	 * @param index Index at which to insert the first element from the specified collection.
+	 * @param c Collection containing elements to be added to this list.
+	 * @return <jk>true</jk> if this list changed as a result of the call.
+	 */
+	public boolean overrideAddAll(int index, Collection<? extends E> c) {
+		return super.addAll(index, c);
+	}
+
+	@Override
+	public void replaceAll(UnaryOperator<E> operator) {
+		checkModifiable();
+		overrideReplaceAll(operator);
+	}
+
+	/**
+	 * Same as {@link #replaceAll(UnaryOperator)} but bypasses the modifiable flag.
+	 *
+	 * @param operator The operator to apply to each element.
+	 */
+	public void overrideReplaceAll(UnaryOperator<E> operator) {
+		super.replaceAll(operator);
+	}
+
+	@Override
+	public void sort(Comparator<? super E> c) {
+		checkModifiable();
+		overrideSort(c);
+	}
+
+	/**
+	 * Same as {@link #overrideSort(Comparator)} but bypasses the modifiable flag.
+	 *
+	 * @param c The Comparator used to compare list elements. A null value indicates that the elements' natural ordering should be used.
+	 */
+	public void overrideSort(Comparator<? super E> c) {
+		super.sort(c);
+	}
+
+	@Override
+	public boolean add(E element) {
+		checkModifiable();
+		return overrideAdd(element);
+	}
+
+	/**
+	 * Same as {@link #add(Object)} but bypasses the modifiable flag.
+	 *
+	 * @param element Element to be stored at the specified position.
+	 * @return <jk>true</jk>.
+	 */
+	public boolean overrideAdd(E element) {
+		return super.add(element);
+	}
+
+	@Override
+	public boolean remove(Object o) {
+		checkModifiable();
+		return overrideRemove(o);
+	}
+
+	/**
+	 * Same as {@link #remove(Object)} but bypasses the modifiable flag.
+	 *
+	 * @param o Element to be removed from this list, if present.
+	 * @return <jk>true</jk> if this list contained the specified element.
+	 */
+	public boolean overrideRemove(Object o) {
+		return super.remove(o);
+	}
+
+	@Override
+	public boolean addAll(Collection<? extends E> c) {
+		checkModifiable();
+		return overrideAddAll(c);
+	}
+
+	/**
+	 * Same as {@link #addAll(Collection)} but bypasses the modifiable flag.
+	 *
+	 * @param c Collection containing elements to be added to this list.
+	 * @return <jk>true</jk> if this list changed as a result of the call.
+	 */
+	public boolean overrideAddAll(Collection<? extends E> c) {
+		return super.addAll(c);
+	}
+
+	@Override
+	public boolean removeAll(Collection<?> coll) {
+		checkModifiable();
+		return overrideRemoveAll(coll);
+	}
+
+	/**
+	 * Same as {@link #removeAll(Collection)} but bypasses the modifiable flag.
+	 *
+	 * @param c Collection containing elements to be removed from this list.
+	 * @return <jk>true</jk> if this list changed as a result of the call.
+	 */
+	public boolean overrideRemoveAll(Collection<?> c) {
+		return super.removeAll(c);
+	}
+
+	@Override
+	public boolean retainAll(Collection<?> c) {
+		checkModifiable();
+		return overrideRetainAll(c);
+	}
+
+	/**
+	 * Same as {@link #retainAll(Collection)} but bypasses the modifiable flag.
+	 *
+	 * @param c Collection containing elements to be retained in this list.
+	 * @return <jk>true</jk> if this list changed as a result of the call.
+	 */
+	public boolean overrideRetainAll(Collection<?> c) {
+		return super.retainAll(c);
+	}
+
+	@Override
+	public void clear() {
+		checkModifiable();
+		overrideClear();
+	}
+
+	/**
+	 * Same as {@link #clear()} but bypasses the modifiable flag.
+	 */
+	public void overrideClear() {
+		super.clear();
+	}
+
+	@Override
+	public boolean removeIf(Predicate<? super E> filter) {
+		checkModifiable();
+		return overrideRemoveIf(filter);
+	}
+
+	/**
+	 * Same as {@link #removeIf(Predicate)} but bypasses the modifiable flag.
+	 *
+	 * @param filter A predicate which returns true for elements to be removed.
+	 * @return <jk>true</jk> if any elements were removed.
+	 */
+	public boolean overrideRemoveIf(Predicate<? super E> filter) {
+		return super.removeIf(filter);
+	}
+
+	@Override
+	public List<E> subList(int fromIndex, int toIndex) {
+		return new ControlledArrayList<>(modifiable, super.subList(fromIndex, toIndex));
+	}
+
+	@Override
+	public ListIterator<E> listIterator() {
+		return listIterator(0);
+	}
+
+	@Override
+	public ListIterator<E> listIterator(final int index) {
+		if (modifiable)
+			return overrideListIterator(index);
+
+		return new ListIterator<E>() {
+			private final ListIterator<? extends E> i = overrideListIterator(index);
+
+			@Override
+			public boolean hasNext() {
+				return i.hasNext();
+			}
+
+			@Override
+			public E next() {
+				return i.next();
+			}
+
+			@Override
+			public boolean hasPrevious() {
+				return i.hasPrevious();
+			}
+
+			@Override
+			public E previous() {
+				return i.previous();
+			}
+
+			@Override
+			public int nextIndex() {
+				return i.nextIndex();
+			}
+
+			@Override
+			public int previousIndex() {
+				return i.previousIndex();
+			}
+
+			@Override
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+
+			@Override
+			public void set(E e) {
+				throw new UnsupportedOperationException();
+			}
+
+			@Override
+			public void add(E e) {
+				throw new UnsupportedOperationException();
+			}
+
+			@Override
+			public void forEachRemaining(Consumer<? super E> action) {
+				i.forEachRemaining(action);
+			}
+		};
+	}
+
+	/**
+	 * Same as {@link #listIterator()} but bypasses the modifiable flag.
+	 *
+	 * @param index Index of the first element to be returned from the list iterator.
+	 * @return A list iterator over the elements in this list (in proper sequence), starting at the specified position in the list.
+	 */
+	public ListIterator<E> overrideListIterator(final int index) {
+		return super.listIterator(index);
+	}
+
+	@Override
+	public Iterator<E> iterator() {
+		if (modifiable)
+			return overrideIterator();
+
+		return new Iterator<E>() {
+			private final Iterator<? extends E> i = overrideIterator();
+
+			@Override
+			public boolean hasNext() {
+				return i.hasNext();
+			}
+
+			@Override
+			public E next() {
+				return i.next();
+			}
+
+			@Override
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+
+			@Override
+			public void forEachRemaining(Consumer<? super E> action) {
+				i.forEachRemaining(action);
+			}
+		};
+	}
+
+	/**
+	 * Same as {@link #iterator()} but bypasses the modifiable flag.
+	 *
+	 * @return An iterator over the elements in this list in proper sequence.
+	 */
+	public Iterator<E> overrideIterator() {
+		return super.iterator();
+	}
+}
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 f26e9a472..b4c1a3493 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
@@ -6777,7 +6777,7 @@ public class RestClient extends BeanContextable implements HttpClient, Closeable
 			if (body instanceof NameValuePair[])
 				return req.content(new UrlEncodedFormEntity(alist((NameValuePair[])body)));
 			if (body instanceof PartList)
-				return req.content(new UrlEncodedFormEntity(((PartList)body).toNameValuePairs()));
+				return req.content(new UrlEncodedFormEntity(((PartList)body)));
 			if (body instanceof HttpResource)
 				((HttpResource)body).getHeaders().forEach(x-> req.header(x));
 			if (body instanceof HttpEntity) {
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/part/PartList.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/part/PartList.java
index f0ca32b4b..e57bc67c3 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/part/PartList.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/part/PartList.java
@@ -24,6 +24,7 @@ import java.util.stream.*;
 import org.apache.http.*;
 import org.apache.http.util.*;
 import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
 import org.apache.juneau.http.HttpParts;
 import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.internal.*;
@@ -156,21 +157,27 @@ import org.apache.juneau.svl.*;
  * 	<li class='extlink'>{@source}
  * </ul>
  */
-public class PartList {
+public class PartList extends ControlledArrayList<NameValuePair> {
 
 	//-----------------------------------------------------------------------------------------------------------------
 	// Static
 	//-----------------------------------------------------------------------------------------------------------------
 
+	private static final long serialVersionUID = 1L;
 	private static final NameValuePair[] EMPTY_ARRAY = new NameValuePair[0];
 	private static final String[] EMPTY_STRING_ARRAY = new String[0];
 	private static final Predicate<NameValuePair> NOT_NULL = x -> x != null;
 
 	/** Represents no part supplier in annotations. */
-	public static final class Null extends PartList {}
+	public static final class Null extends PartList {
+		Null(boolean modifiable) {
+			super(false);
+		}
+		private static final long serialVersionUID = 1L;
+	}
 
 	/** Predefined instance. */
-	public static final PartList EMPTY = new PartList();
+	public static final PartList EMPTY = new PartList(false);
 
 	/**
 	 * Instantiates a new builder for this bean.
@@ -191,7 +198,7 @@ public class PartList {
 	 * @return A new unmodifiable instance, never <jk>null</jk>.
 	 */
 	public static PartList of(List<NameValuePair> parts) {
-		return parts == null || parts.isEmpty() ? EMPTY : new PartList(parts);
+		return parts == null || parts.isEmpty() ? EMPTY : new PartList(true, parts);
 	}
 
 	/**
@@ -203,7 +210,7 @@ public class PartList {
 	 * @return A new unmodifiable instance, never <jk>null</jk>.
 	 */
 	public static PartList of(NameValuePair...parts) {
-		return parts == null || parts.length == 0 ? EMPTY : new PartList(parts);
+		return parts == null || parts.length == 0 ? EMPTY : new PartList(true, parts);
 	}
 
 	/**
@@ -228,7 +235,7 @@ public class PartList {
 		ArrayBuilder<NameValuePair> b = ArrayBuilder.of(NameValuePair.class).filter(NOT_NULL).size(pairs.length / 2);
 		for (int i = 0; i < pairs.length; i+=2)
 			b.add(BasicPart.of(stringify(pairs[i]), pairs[i+1]));
-		return new PartList(b.orElse(EMPTY_ARRAY));
+		return new PartList(true, b.orElse(EMPTY_ARRAY));
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
@@ -244,7 +251,7 @@ public class PartList {
 		final List<NameValuePair> entries;
 		List<NameValuePair> defaultEntries;
 		private VarResolver varResolver;
-		boolean caseInsensitive = false;
+		boolean caseInsensitive = false, unmodifiable = false;
 
 		/**
 		 * Constructor.
@@ -261,8 +268,9 @@ public class PartList {
 		 */
 		protected Builder(PartList copyFrom) {
 			super(copyFrom.getClass());
-			entries = list(copyFrom.entries);
+			entries = copyOf(copyFrom);
 			caseInsensitive = copyFrom.caseInsensitive;
+			unmodifiable = false;
 		}
 
 		/**
@@ -276,6 +284,7 @@ public class PartList {
 			defaultEntries = copyOf(copyFrom.defaultEntries);
 			varResolver = copyFrom.varResolver;
 			caseInsensitive = copyFrom.caseInsensitive;
+			unmodifiable = copyFrom.unmodifiable;
 		}
 
 		@Override /* BeanBuilder */
@@ -359,27 +368,26 @@ public class PartList {
 		}
 
 		/**
-		 * Removes any parts already in this builder.
+		 * Specifies that the resulting list should be unmodifiable.
+		 *
+		 * <p>
+		 * The default behavior is modifiable.
 		 *
 		 * @return This object.
 		 */
-		@FluentSetter
-		public Builder clear() {
-			entries.clear();
+		public Builder unmodifiable() {
+			unmodifiable = true;
 			return this;
 		}
 
 		/**
-		 * Adds the specified parts to the end of the parts in this builder.
+		 * Removes any parts already in this builder.
 		 *
-		 * @param value The parts to add.  <jk>null</jk> values are ignored.
 		 * @return This object.
 		 */
 		@FluentSetter
-		public Builder append(PartList value) {
-			if (value != null)
-				for (NameValuePair x : value.entries)
-					append(x);
+		public Builder clear() {
+			entries.clear();
 			return this;
 		}
 
@@ -450,8 +458,7 @@ public class PartList {
 		@FluentSetter
 		public Builder append(List<? extends NameValuePair> values) {
 			if (values != null)
-				for (int i = 0, j = values.size(); i < j; i++)
-					append(values.get(i));
+				values.forEach(x -> append(x));
 			return this;
 		}
 
@@ -464,7 +471,7 @@ public class PartList {
 		@FluentSetter
 		public Builder prepend(PartList value) {
 			if (value != null)
-				prependAll(entries, value.entries);
+				prependAll(entries, value.getAll());
 			return this;
 		}
 
@@ -538,20 +545,6 @@ public class PartList {
 			return this;
 		}
 
-		/**
-		 * Removes the specified part from this builder.
-		 *
-		 * @param value The part to remove.  <jk>null</jk> values are ignored.
-		 * @return This object.
-		 */
-		@FluentSetter
-		public Builder 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.
 		 *
@@ -586,8 +579,8 @@ public class PartList {
 		 */
 		@FluentSetter
 		public Builder remove(List<? extends NameValuePair> values) {
-			for (int i = 0, j = values.size(); i < j; i++) /* See HTTPCORE-361 */
-				remove(values.get(i));
+			if (values != null)
+				values.forEach(x -> remove(x));
 			return this;
 		}
 
@@ -727,21 +720,6 @@ public class PartList {
 			return this;
 		}
 
-		/**
-		 * Adds or replaces the parts with the specified names.
-		 *
-		 * <p>
-		 * 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 replace.  <jk>null</jk> values are ignored.
-		 * @return This object.
-		 */
-		public Builder set(PartList values) {
-			if (values != null)
-				set(values.entries);
-			return this;
-		}
-
 		/**
 		 * Sets a default value for a part.
 		 *
@@ -852,21 +830,6 @@ public class PartList {
 			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.
-		 */
-		public Builder setDefault(PartList values) {
-			if (values != null)
-				setDefault(values.entries);
-			return this;
-		}
-
 		/**
 		 * Adds the specify part to this list.
 		 *
@@ -1007,33 +970,6 @@ public class PartList {
 			throw new BasicRuntimeException("Invalid value specified for flag parameter on add(flag,values) method: {0}", flag);
 		}
 
-		/**
-		 * Adds the specified parts to this list.
-		 *
-		 * @param flag
-		 * 	What to do with the part.
-		 * 	<br>Possible values:
-		 * 	<ul>
-		 * 		<li>{@link ListOperation#APPEND APPEND} - Calls {@link #append(PartList)}.
-		 * 		<li>{@link ListOperation#PREPEND PREEND} - Calls {@link #prepend(PartList)}.
-		 * 		<li>{@link ListOperation#SET REPLACE} - Calls {@link #set(PartList)}.
-		 * 		<li>{@link ListOperation#DEFAULT DEFAULT} - Calls {@link #setDefault(PartList)}.
-		 * 	</ul>
-		 * @param values The parts to add.
-		 * @return This object.
-		 */
-		public Builder add(ListOperation flag, PartList values) {
-			if (flag == ListOperation.APPEND)
-				return append(values);
-			if (flag == ListOperation.PREPEND)
-				return prepend(values);
-			if (flag == ListOperation.SET)
-				return set(values);
-			if (flag == ListOperation.DEFAULT)
-				return setDefault(values);
-			throw new BasicRuntimeException("Invalid value specified for flag parameter on add(flag,values) method: {0}", flag);
-		}
-
 		/**
 		 * Performs an action on all the parts in this list.
 		 *
@@ -1177,7 +1113,6 @@ public class PartList {
 	// Instance
 	//-----------------------------------------------------------------------------------------------------------------
 
-	final NameValuePair[] entries;
 	final boolean caseInsensitive;
 
 	/**
@@ -1186,24 +1121,17 @@ public class PartList {
 	 * @param builder The builder containing the settings for this bean.
 	 */
 	public PartList(Builder builder) {
-		if (builder.defaultEntries == null) {
-			entries = builder.entries.toArray(new NameValuePair[builder.entries.size()]);
-		} else {
-			ArrayBuilder<NameValuePair> l = ArrayBuilder.of(NameValuePair.class).filter(NOT_NULL).size(builder.entries.size() + builder.defaultEntries.size());
-
-			for (int i = 0, j = builder.entries.size(); i < j; i++)
-				l.add(builder.entries.get(i));
+		super(! builder.unmodifiable, builder.entries);
 
+		if (builder.defaultEntries != null) {
 			for (int i1 = 0, j1 = builder.defaultEntries.size(); i1 < j1; i1++) {
 				NameValuePair 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);
+					overrideAdd(x);
 			}
-
-			entries = l.orElse(EMPTY_ARRAY);
 		}
 		this.caseInsensitive = builder.caseInsensitive;
 	}
@@ -1211,39 +1139,37 @@ public class PartList {
 	/**
 	 * Constructor.
 	 *
+	 * @param modifiable Whether this list should be modifiable.
 	 * @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<NameValuePair> parts) {
-		ArrayBuilder<NameValuePair> l = ArrayBuilder.of(NameValuePair.class).filter(NOT_NULL).size(parts.size());
-		for (int i = 0, j = parts.size(); i < j; i++)
-			l.add(parts.get(i));
-		entries = l.orElse(EMPTY_ARRAY);
+	protected PartList(boolean modifiable, List<NameValuePair> parts) {
+		super(modifiable, parts);
 		caseInsensitive = false;
 	}
 
 	/**
 	 * Constructor.
 	 *
+	 * @param modifiable Whether this list should be modifiable.
 	 * @param parts
 	 * 	The parts to add to the list.
 	 * 	<br><jk>null</jk> entries are ignored.
 	 */
-	protected PartList(NameValuePair...parts) {
-		ArrayBuilder<NameValuePair> l = ArrayBuilder.of(NameValuePair.class).filter(NOT_NULL).size(parts.length);
-		for (int i = 0; i < parts.length; i++)
-			l.add(parts[i]);
-		entries = l.orElse(EMPTY_ARRAY);
+	protected PartList(boolean modifiable, NameValuePair...parts) {
+		super(modifiable, Arrays.asList(parts));
 		caseInsensitive = false;
 	}
 
 	/**
 	 * Default constructor.
+	 *
+	 * @param modifiable Whether this list should be modifiable.
 	 */
-	protected PartList() {
-		entries = EMPTY_ARRAY;
+	protected PartList(boolean modifiable) {
+		super(modifiable);
 		caseInsensitive = false;
 	}
 
@@ -1270,8 +1196,7 @@ public class PartList {
 
 		NameValuePair first = null;
 		List<NameValuePair> rest = null;
-		for (int i = 0; i < entries.length; i++) {
-			NameValuePair x = entries[i];
+		for (NameValuePair x : this) {
 			if (eq(x.getName(), name)) {
 				if (first == null)
 					first = x;
@@ -1329,8 +1254,7 @@ public class PartList {
 
 		NameValuePair first = null;
 		List<NameValuePair> rest = null;
-		for (int i = 0; i < entries.length; i++) {
-			NameValuePair x = entries[i];
+		for (NameValuePair x : this) {
 			if (eq(x.getName(), name)) {
 				if (first == null)
 					first = x;
@@ -1393,7 +1317,7 @@ public class PartList {
 	 * 	An array of all the parts in this list, or an empty array if no parts are present.
 	 */
 	public NameValuePair[] getAll() {
-		return entries.length == 0 ? EMPTY_ARRAY : Arrays.copyOf(entries, entries.length);
+		return size() == 0 ? EMPTY_ARRAY : toArray(new NameValuePair[size()]);
 	}
 
 	/**
@@ -1410,21 +1334,12 @@ public class PartList {
 	 */
 	public NameValuePair[] getAll(String name) {
 		ArrayBuilder<NameValuePair> b = ArrayBuilder.of(NameValuePair.class).filter(NOT_NULL);
-		for (int i = 0; i < entries.length; i++)
-			if (eq(entries[i].getName(), name))
-				b.add(entries[i]);
+		for (NameValuePair x : this)
+			if (eq(x.getName(), name))
+				b.add(x);
 		return b.orElse(EMPTY_ARRAY);
 	}
 
-	/**
-	 * 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 part with the given name.
 	 *
@@ -1435,8 +1350,7 @@ public class PartList {
 	 * @return The first matching part, or <jk>null</jk> if not found.
 	 */
 	public Optional<NameValuePair> getFirst(String name) {
-		for (int i = 0; i < entries.length; i++) {
-			NameValuePair x = entries[i];
+		for (NameValuePair x : this) {
 			if (eq(x.getName(), name))
 				return optional(x);
 		}
@@ -1453,8 +1367,8 @@ public class PartList {
 	 * @return The last matching part, or <jk>null</jk> if not found.
 	 */
 	public Optional<NameValuePair> getLast(String name) {
-		for (int i = entries.length - 1; i >= 0; i--) {
-			NameValuePair x = entries[i];
+		for (int i = size() - 1; i >= 0; i--) {
+			NameValuePair x = get(i);
 			if (eq(x.getName(), name))
 				return optional(x);
 		}
@@ -1505,8 +1419,7 @@ public class PartList {
 	 * @return <jk>true</jk> if at least one part with the name is present.
 	 */
 	public boolean contains(String name) {
-		for (int i = 0; i < entries.length; i++) {
-			NameValuePair x = entries[i];
+		for (NameValuePair x : this) {
 			if (eq(x.getName(), name))
 				return true;
 		}
@@ -1518,8 +1431,9 @@ public class PartList {
 	 *
 	 * @return A new iterator over this list of parts.
 	 */
+	@Override
 	public PartIterator iterator() {
-		return new BasicPartIterator(entries, null, caseInsensitive);
+		return new BasicPartIterator(getAll(), null, caseInsensitive);
 	}
 
 	/**
@@ -1530,21 +1444,7 @@ public class PartList {
 	 * @return A new iterator over the matching parts in this list.
 	 */
 	public PartIterator iterator(String name) {
-		return new BasicPartIterator(entries, name, caseInsensitive);
-	}
-
-	/**
-	 * Performs an action on all parts 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 action An action to perform on each element.
-	 * @return This object.
-	 */
-	public PartList forEach(Consumer<NameValuePair> action) {
-		return forEach(x -> true, action);
+		return new BasicPartIterator(getAll(), name, caseInsensitive);
 	}
 
 	/**
@@ -1574,23 +1474,10 @@ public class PartList {
 	 * @return This object.
 	 */
 	public PartList forEach(Predicate<NameValuePair> filter, Consumer<NameValuePair> action) {
-		for (int i = 0; i < entries.length; i++)
-			consume(filter, action, entries[i]);
+		forEach(x -> consume(filter, action, x));
 		return this;
 	}
 
-	/**
-	 * Returns a stream of the parts in this list.
-	 *
-	 * <p>
-	 * This does not involve a copy of the underlying array of <c>NameValuePair</c> objects so should perform well.
-	 *
-	 * @return This object.
-	 */
-	public Stream<NameValuePair> stream() {
-		return Arrays.stream(entries);
-	}
-
 	/**
 	 * Returns a stream of the parts in this list with the specified name.
 	 *
@@ -1601,28 +1488,7 @@ public class PartList {
 	 * @return This object.
 	 */
 	public Stream<NameValuePair> 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> toNameValuePairs() {
-		return ulist(entries);
-	}
-
-	/**
-	 * Performs an action on the contents of this list.
-	 *
-	 * @param action The action to perform.
-	 * @return This object.
-	 */
-	public PartList forEachNameValuePair(Consumer<NameValuePair> action) {
-		for (NameValuePair p : entries)
-			action.accept(p);
-		return this;
+		return Arrays.stream(getAll(name)).filter(x->eq(name, x.getName()));
 	}
 
 	private boolean eq(String s1, String s2) {
@@ -1635,15 +1501,16 @@ public class PartList {
 	@Override /* Object */
 	public String toString() {
 		StringBuilder sb = new StringBuilder();
-		for (int i = 0; i < entries.length; i++) {
-			NameValuePair p = entries[i];
-			String v = p.getValue();
-			if (v != null) {
-				if (sb.length() > 0)
-					sb.append("&");
-				sb.append(urlEncode(p.getName())).append('=').append(urlEncode(p.getValue()));
+		forEach(p -> {
+			if (p != null) {
+				String v = p.getValue();
+				if (v != null) {
+					if (sb.length() > 0)
+						sb.append("&");
+					sb.append(urlEncode(p.getName())).append('=').append(urlEncode(p.getValue()));
+				}
 			}
-		}
+		});
 		return sb.toString();
 	}
 }
diff --git a/juneau-utest/src/test/java/org/apache/juneau/utils/ArgsTest.java b/juneau-utest/src/test/java/org/apache/juneau/collections/Args_Test.java
similarity index 88%
rename from juneau-utest/src/test/java/org/apache/juneau/utils/ArgsTest.java
rename to juneau-utest/src/test/java/org/apache/juneau/collections/Args_Test.java
index ace209ace..0a9dd9865 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/utils/ArgsTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/collections/Args_Test.java
@@ -10,22 +10,21 @@
 // * "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;
+package org.apache.juneau.collections;
 
 import static org.junit.Assert.*;
 import static org.junit.runners.MethodSorters.*;
 
-import org.apache.juneau.collections.*;
 import org.junit.*;
 
 @FixMethodOrder(NAME_ASCENDING)
-public class ArgsTest {
+public class Args_Test {
 
-	//====================================================================================================
+	//-----------------------------------------------------------------------------------------------------------------
 	// test - Basic tests
-	//====================================================================================================
+	//-----------------------------------------------------------------------------------------------------------------
 	@Test
-	public void test() throws Exception {
+	public void basic() throws Exception {
 		Args a;
 
 		// Empty args
diff --git a/juneau-utest/src/test/java/org/apache/juneau/collections/ControlledArrayList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/collections/ControlledArrayList_Test.java
new file mode 100644
index 000000000..d568f5da0
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/collections/ControlledArrayList_Test.java
@@ -0,0 +1,191 @@
+// ***************************************************************************************************************************
+// * 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.collections;
+
+import static org.apache.juneau.assertions.Assertions.*;
+import static org.junit.Assert.*;
+import static org.junit.runners.MethodSorters.*;
+
+import java.util.*;
+
+import org.junit.*;
+
+@FixMethodOrder(NAME_ASCENDING)
+public class ControlledArrayList_Test {
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// test - Basic tests
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void a01_constructors() throws Exception {
+		ControlledArrayList<Integer> x;
+
+		x = new ControlledArrayList<>(true);
+		assertTrue(x.isModifiable());
+
+		x = new ControlledArrayList<>(false);
+		assertFalse(x.isModifiable());
+
+		x = new ControlledArrayList<>(true, Arrays.asList(1));
+		assertTrue(x.isModifiable());
+
+		x = new ControlledArrayList<>(false, Arrays.asList(1));
+		assertFalse(x.isModifiable());
+	}
+
+	@Test
+	public void a02_basicMethods() throws Exception {
+		ControlledArrayList<Integer> x1 = new ControlledArrayList<Integer>(true, Arrays.asList(1));
+		ControlledArrayList<Integer> x2 = new ControlledArrayList<Integer>(false, Arrays.asList(1));
+
+		x1.set(0, 2);
+		assertThrown(() -> x2.set(0, 2)).isType(UnsupportedOperationException.class);
+		x2.overrideSet(0, 2);
+		assertList(x1).is(x2);
+
+		x1.add(0, 2);
+		assertThrown(() -> x2.add(0, 2)).isType(UnsupportedOperationException.class);
+		x2.overrideAdd(0, 2);
+		assertList(x1).is(x2);
+
+		x1.remove(0);
+		assertThrown(() -> x2.remove(0)).isType(UnsupportedOperationException.class);
+		x2.overrideRemove(0);
+		assertList(x1).is(x2);
+
+		x1.addAll(0, Arrays.asList(3));
+		assertThrown(() -> x2.addAll(0, Arrays.asList(3))).isType(UnsupportedOperationException.class);
+		x2.overrideAddAll(0, Arrays.asList(3));
+		assertList(x1).is(x2);
+
+		x1.replaceAll(x -> x);
+		assertThrown(() -> x2.replaceAll(x -> x)).isType(UnsupportedOperationException.class);
+		x2.overrideReplaceAll(x -> x);
+		assertList(x1).is(x2);
+
+		x1.sort(null);
+		assertThrown(() -> x2.sort(null)).isType(UnsupportedOperationException.class);
+		x2.overrideSort(null);
+		assertList(x1).is(x2);
+
+		x1.add(1);
+		assertThrown(() -> x2.add(1)).isType(UnsupportedOperationException.class);
+		x2.overrideAdd(1);
+		assertList(x1).is(x2);
+
+		x1.remove((Integer)1);
+		assertThrown(() -> x2.remove((Integer)1)).isType(UnsupportedOperationException.class);
+		x2.overrideRemove((Integer)1);
+		assertList(x1).is(x2);
+
+		x1.addAll(Arrays.asList(3));
+		assertThrown(() -> x2.addAll(Arrays.asList(3))).isType(UnsupportedOperationException.class);
+		x2.overrideAddAll(Arrays.asList(3));
+		assertList(x1).is(x2);
+
+		x1.removeAll(Arrays.asList(3));
+		assertThrown(() -> x2.removeAll(Arrays.asList(3))).isType(UnsupportedOperationException.class);
+		x2.overrideRemoveAll(Arrays.asList(3));
+		assertList(x1).is(x2);
+
+		x1.retainAll(Arrays.asList(2));
+		assertThrown(() -> x2.retainAll(Arrays.asList(2))).isType(UnsupportedOperationException.class);
+		x2.overrideRetainAll(Arrays.asList(2));
+		assertList(x1).is(x2);
+
+		x1.clear();
+		assertThrown(() -> x2.clear()).isType(UnsupportedOperationException.class);
+		x2.overrideClear();
+		assertList(x1).is(x2);
+
+		x1.add(1);
+		x2.overrideAdd(1);
+
+		x1.removeIf(x -> x == 1);
+		assertThrown(() -> x2.removeIf(x -> x == 1)).isType(UnsupportedOperationException.class);
+		x2.overrideRemoveIf(x -> x == 1);
+		assertList(x1).is(x2);
+
+		x1.add(1);
+		x2.overrideAdd(1);
+
+		ControlledArrayList<Integer> x1a = (ControlledArrayList<Integer>) x1.subList(0, 0);
+		ControlledArrayList<Integer> x2a = (ControlledArrayList<Integer>) x2.subList(0, 0);
+		assertTrue(x1a.isModifiable());
+		assertFalse(x2a.isModifiable());
+	}
+
+	@Test
+	public void a03_iterator() throws Exception {
+		ControlledArrayList<Integer> x1 = new ControlledArrayList<Integer>(true, Arrays.asList(1));
+		ControlledArrayList<Integer> x2 = new ControlledArrayList<Integer>(false, Arrays.asList(1));
+
+		Iterator<Integer> i1 = x1.iterator();
+		Iterator<Integer> i2 = x2.iterator();
+
+		assertTrue(i1.hasNext());
+		assertTrue(i2.hasNext());
+
+		assertEquals(1, i1.next().intValue());
+		assertEquals(1, i2.next().intValue());
+
+		i1.remove();
+		assertThrown(() -> i2.remove()).isType(UnsupportedOperationException.class);
+
+		i1.forEachRemaining(x -> {});
+		i2.forEachRemaining(x -> {});
+	}
+
+	@Test
+	public void a04_listIterator() throws Exception {
+		ControlledArrayList<Integer> x1 = new ControlledArrayList<Integer>(true, Arrays.asList(1));
+		ControlledArrayList<Integer> x2 = new ControlledArrayList<Integer>(false, Arrays.asList(1));
+
+		ListIterator<Integer> i1a = x1.listIterator();
+		ListIterator<Integer> i2a = x2.listIterator();
+
+		assertTrue(i1a.hasNext());
+		assertTrue(i2a.hasNext());
+
+		assertEquals(1, i1a.next().intValue());
+		assertEquals(1, i2a.next().intValue());
+
+		assertTrue(i1a.hasPrevious());
+		assertTrue(i2a.hasPrevious());
+
+		assertEquals(1, i1a.nextIndex());
+		assertEquals(1, i2a.nextIndex());
+
+		assertEquals(0, i1a.previousIndex());
+		assertEquals(0, i2a.previousIndex());
+
+		i1a.previous();
+		i2a.previous();
+
+		i1a.set(1);
+		assertThrown(() -> i2a.set(1)).isType(UnsupportedOperationException.class);
+
+		i1a.add(1);
+		assertThrown(() -> i2a.add(1)).isType(UnsupportedOperationException.class);
+
+		i1a.next();
+		i2a.next();
+
+		i1a.remove();
+		assertThrown(() -> i2a.remove()).isType(UnsupportedOperationException.class);
+
+		i1a.forEachRemaining(x -> {});
+		i2a.forEachRemaining(x -> {});
+	}
+}
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
index 34b679cbd..bb2fe44f1 100644
--- 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
@@ -92,7 +92,7 @@ public class PartList_Test {
 		x.append((List<NameValuePair>)null);
 		assertObject(x.build()).isString("Foo=1&Foo=2&Foo=3&Foo=4&Foo=5&Foo=6&Foo=7");
 
-		assertObject(new PartList.Null()).isString("");
+		assertObject(new PartList.Null(false)).isString("");
 	}
 
 	@Test
@@ -558,16 +558,6 @@ public class PartList_Test {
 		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.toNameValuePairs()).isString("[a=x]");
-	}
-
 	//-----------------------------------------------------------------------------------------------------------------
 	// Utility methods
 	//-----------------------------------------------------------------------------------------------------------------