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 2026/07/21 14:37:01 UTC

(juneau) branch master updated: TODO-274: CRTP-convert the classic BasicResource hierarchy (10.0.0 breaking change)

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 d0eaf21f4d TODO-274: CRTP-convert the classic BasicResource hierarchy (10.0.0 breaking change)
d0eaf21f4d is described below

commit d0eaf21f4daa8ad5fc45aebe3cd4d9030ec0177b
Author: James Bognar <ja...@apache.org>
AuthorDate: Tue Jul 21 10:36:51 2026 -0400

    TODO-274: CRTP-convert the classic BasicResource hierarchy (10.0.0 breaking change)
    
    Generify BasicResource into an abstract self-typed (CRTP) root
    BasicResource<SELF extends BasicResource<SELF>> implements HttpResource, mirroring
    the already-shipped BasicHttpEntity<SELF> precedent. Each of the 5 leaves
    (ByteArrayResource, FileResource, ReaderResource, StreamResource, StringResource)
    drops its 15 covariant narrowing-setter overrides (~75 total deleted) and inherits
    leaf-typed fluent setters for free.
    
    Generifying makes the root effectively abstract, so ResponseContent now materializes
    HttpResource/BasicResource response bodies as a concrete StreamResource (which IS-A
    BasicResource), and StreamResource gained an HttpResponse constructor for this. This
    keeps the breaking surface narrow: .as(BasicResource.class) and @Remote methods
    declared to return BasicResource/HttpResource still work; only direct/reflective
    new BasicResource(...) instantiation breaks. BasicResource_Test's direct-instantiation
    sites were rebased onto concrete leaves.
    
    Co-authored-by: Cursor <cu...@cursor.com>
---
 .../rest/client/classic/ResponseContent.java       |   8 +-
 .../http/classic/resource/BasicResource.java       | 136 +++++++++------------
 .../http/classic/resource/ByteArrayResource.java   |  98 +--------------
 .../juneau/http/classic/resource/FileResource.java |  97 +--------------
 .../http/classic/resource/ReaderResource.java      |  97 +--------------
 .../http/classic/resource/StreamResource.java      | 105 ++--------------
 .../http/classic/resource/StringResource.java      |  98 +--------------
 .../http/classic/resource/BasicResource_Test.java  |  59 +++++----
 8 files changed, 114 insertions(+), 584 deletions(-)

diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
index b6be172e19..41f3d19432 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
@@ -162,7 +162,7 @@ public class ResponseContent implements HttpEntity {
 	 * 			<li>{@link ResponseContent}/{@link HttpEntity} - Returns access to this object.
 	 * 			<li>{@link Reader} - Returns access to the raw reader of the response.
 	 * 			<li>{@link InputStream} - Returns access to the raw input stream of the response.
-	 * 			<li>{@link HttpResource} - Response will be converted to an {@link BasicResource}.
+	 * 			<li>{@link HttpResource} - Response will be converted to a {@link StreamResource}.
 	 * 			<li>Any type that takes in an {@link HttpResponse} object.
 	 * 		</ul>
 	 * 	<li class='note'>
@@ -254,8 +254,8 @@ public class ResponseContent implements HttpEntity {
 			if (type.is(HttpResponse.class))
 				return (T)response;
 
-			if (type.is(HttpResource.class))
-				type = (ClassMeta<T>)getClassMeta(BasicResource.class);
+			if (type.is(HttpResource.class) || type.is(BasicResource.class))
+				type = (ClassMeta<T>)getClassMeta(StreamResource.class);
 
 			var result = type.getPublicConstructor(x -> x.hasParameterTypes(HttpResponse.class)).map(ci -> safe(() -> (T)ci.newInstance(response)));
 			if (result.isPresent())
@@ -429,7 +429,7 @@ public class ResponseContent implements HttpEntity {
 	 * 			<li>{@link ResponseContent}/{@link HttpEntity} - Returns access to this object.
 	 * 			<li>{@link Reader} - Returns access to the raw reader of the response.
 	 * 			<li>{@link InputStream} - Returns access to the raw input stream of the response.
-	 * 			<li>{@link HttpResource} - Response will be converted to an {@link BasicResource}.
+	 * 			<li>{@link HttpResource} - Response will be converted to a {@link StreamResource}.
 	 * 			<li>Any type that takes in an {@link HttpResponse} object.
 	 * 		</ul>
 	 * 	<li class='note'>
diff --git a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/BasicResource.java b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/BasicResource.java
index 284a5e0a57..07adaa327e 100644
--- a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/BasicResource.java
+++ b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/BasicResource.java
@@ -44,16 +44,23 @@ import org.apache.juneau.test.assertions.*;
  * 		Externally-supplied/dynamic content.
  * </ul>
  *
+ * <p>
+ * This is a self-typed (CRTP) root: a leaf declares <c>class StringResource <jk>extends</jk> BasicResource&lt;StringResource&gt;</c>
+ * and inherits leaf-typed fluent setters with no covariant overrides.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common Basics</a>
  * </ul>
+ *
+ * @param <SELF> The self type for fluent setters.
  */
 @BeanIgnore /* Use toString() to serialize */
 @SuppressWarnings({
 	"resource", // Depends on entity (streams); value equality not practical
 	"java:S1206", // equals/hashCode not overridden; value equality not practical for this class
+	"java:S119", // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
 })
-public class BasicResource implements HttpResource {
+public abstract class BasicResource<SELF extends BasicResource<SELF>> implements HttpResource {
 	BasicHttpEntity<?> entity;
 	HeaderList headers = HeaderList.create();
 
@@ -71,7 +78,7 @@ public class BasicResource implements HttpResource {
 	 *
 	 * @param copyFrom The bean being copied.  Must not be <jk>null</jk>.
 	 */
-	public BasicResource(BasicResource copyFrom) {
+	public BasicResource(BasicResource<?> copyFrom) {
 		this.entity = copyFrom.entity.copy();
 		this.headers = copyFrom.headers.copy();
 	}
@@ -100,10 +107,10 @@ public class BasicResource implements HttpResource {
 	 * @param value The header value.
 	 * @return This object.
 	 */
-	public BasicResource addHeader(String name, String value) {
+	public SELF addHeader(String name, String value) {
 		if (nn(name) && nn(value))
 			headers.append(name, value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -112,7 +119,7 @@ public class BasicResource implements HttpResource {
 	 * @param values The headers to set.  <jk>null</jk> headers and headers with <jk>null</jk> names or values are ignored.
 	 * @return This object.
 	 */
-	public BasicResource addHeaders(Header...values) {
+	public SELF addHeaders(Header...values) {
 		for (var h : values) {
 			if (nn(h)) {
 				var n = h.getName();
@@ -129,7 +136,7 @@ public class BasicResource implements HttpResource {
 				}
 			}
 		}
-		return this;
+		return self();
 	}
 
 	/**
@@ -154,7 +161,7 @@ public class BasicResource implements HttpResource {
 	 * @return A new fluent assertion.
 	 * @throws IOException If a problem occurred while trying to read the byte array.
 	 */
-	public FluentByteArrayAssertion<BasicResource> assertBytes() throws IOException {
+	public FluentByteArrayAssertion<BasicResource<?>> assertBytes() throws IOException {
 		return new FluentByteArrayAssertion<>(asBytes(), this);
 	}
 
@@ -167,7 +174,7 @@ public class BasicResource implements HttpResource {
 	 * @return A new fluent assertion.
 	 * @throws IOException If a problem occurred while trying to read the byte array.
 	 */
-	public FluentStringAssertion<BasicResource> assertString() throws IOException {
+	public FluentStringAssertion<BasicResource<?>> assertString() throws IOException {
 		return new FluentStringAssertion<>(asString(), this);
 	}
 
@@ -197,9 +204,7 @@ public class BasicResource implements HttpResource {
 	 *
 	 * @return A new builder bean.
 	 */
-	public BasicResource copy() {
-		return new BasicResource(this);
-	}
+	public abstract SELF copy();
 
 	/**
 	 * Copies the contents of the specified HTTP response to this builder.
@@ -208,11 +213,11 @@ public class BasicResource implements HttpResource {
 	 * @return This object.
 	 * @throws IOException If content could not be retrieved.
 	 */
-	public BasicResource copyFrom(HttpResponse response) throws IOException {
+	public SELF copyFrom(HttpResponse response) throws IOException {
 		assertArgNotNull("response", response);
 		addHeaders(response.getAllHeaders());
 		setContent(response.getEntity().getContent());
-		return this;
+		return self();
 	}
 
 	@Override /* Overridden from HttpEntity */
@@ -260,9 +265,9 @@ public class BasicResource implements HttpResource {
 	 * @return This object.
 	 * @throws IOException If entity could not be read into memory.
 	 */
-	public BasicResource setCached() throws IOException {
+	public SELF setCached() throws IOException {
 		entity.setCached();
-		return this;
+		return self();
 	}
 
 	/**
@@ -275,9 +280,9 @@ public class BasicResource implements HttpResource {
 	 *
 	 * @return This object.
 	 */
-	public BasicResource setChunked() {
+	public SELF setChunked() {
 		entity.setChunked();
-		return this;
+		return self();
 	}
 
 	/**
@@ -291,9 +296,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The new value for this flag.
 	 * @return This object.
 	 */
-	public BasicResource setChunked(boolean value) {
+	public SELF setChunked(boolean value) {
 		entity.setChunked(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -302,9 +307,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The entity content, can be <jk>null</jk>.
 	 * @return This object.
 	 */
-	public BasicResource setContent(Object value) {
+	public SELF setContent(Object value) {
 		entity.setContent(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -317,9 +322,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The entity content, can be <jk>null</jk>.
 	 * @return This object.
 	 */
-	public BasicResource setContent(Supplier<?> value) {
+	public SELF setContent(Supplier<?> value) {
 		entity.setContent(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -328,9 +333,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The new <c>Content-Encoding</c> header, or <jk>null</jk> to unset.
 	 * @return This object.
 	 */
-	public BasicResource setContentEncoding(ContentEncoding value) {
+	public SELF setContentEncoding(ContentEncoding value) {
 		entity.setContentEncoding(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -339,9 +344,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The new <c>Content-Encoding</c> header, or <jk>null</jk> to unset.
 	 * @return This object.
 	 */
-	public BasicResource setContentEncoding(String value) {
+	public SELF setContentEncoding(String value) {
 		entity.setContentEncoding(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -350,9 +355,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The new <c>Content-Length</c> header value, or <c>-1</c> to unset.
 	 * @return This object.
 	 */
-	public BasicResource setContentLength(long value) {
+	public SELF setContentLength(long value) {
 		entity.setContentLength(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -361,9 +366,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The new <c>Content-Type</c> header, or <jk>null</jk> to unset.
 	 * @return This object.
 	 */
-	public BasicResource setContentType(ContentType value) {
+	public SELF setContentType(ContentType value) {
 		entity.setContentType(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -372,9 +377,9 @@ public class BasicResource implements HttpResource {
 	 * @param value The new <c>Content-Type</c> header, or <jk>null</jk> to unset.
 	 * @return This object.
 	 */
-	public BasicResource setContentType(String value) {
+	public SELF setContentType(String value) {
 		entity.setContentType(value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -387,10 +392,10 @@ public class BasicResource implements HttpResource {
 	 * @param value The header value.
 	 * @return This object.
 	 */
-	public BasicResource setHeader(String name, String value) {
+	public SELF setHeader(String name, String value) {
 		if (nn(name) && nn(value))
 			headers.set(name, value);
-		return this;
+		return self();
 	}
 
 	/**
@@ -399,7 +404,7 @@ public class BasicResource implements HttpResource {
 	 * @param values The headers to add.  <jk>null</jk> values are ignored.
 	 * @return This object.
 	 */
-	public BasicResource setHeaders(Header...values) {
+	public SELF setHeaders(Header...values) {
 		for (var h : values) {
 			if (nn(h)) {
 				var n = h.getName();
@@ -416,7 +421,7 @@ public class BasicResource implements HttpResource {
 				}
 			}
 		}
-		return this;
+		return self();
 	}
 
 	/**
@@ -425,7 +430,7 @@ public class BasicResource implements HttpResource {
 	 * @param value The new value.  Must not be <jk>null</jk>.
 	 * @return This object.
 	 */
-	public BasicResource setHeaders(HeaderList value) {
+	public SELF setHeaders(HeaderList value) {
 		return modify(() -> headers = value.copy());
 	}
 
@@ -437,33 +442,41 @@ public class BasicResource implements HttpResource {
 	 *
 	 * @return An unmodifiable snapshot of this bean, or this bean if it is already unmodifiable.
 	 */
-	public BasicResource unmodifiable() {
-		return this instanceof UnmodifiableBean ? this : new Unmodifiable(this);
-	}
+	public abstract SELF unmodifiable();
 
 	@Override /* Overridden from HttpEntity */
 	public void writeTo(OutputStream outStream) throws IOException {
 		entity.writeTo(outStream);
 	}
 
+	/**
+	 * Returns this object cast to the self type.
+	 *
+	 * @return This object.
+	 */
+	@SuppressWarnings({
+		"unchecked" // CRTP self-type cast is safe: SELF is bound to the concrete leaf type.
+	})
+	protected final SELF self() { return (SELF) this; }
+
 	/**
 	 * Single mutation funnel for direct-field state changes on this bean.
 	 *
 	 * <p>
-	 * The nested {@link Unmodifiable} snapshot overrides this method to throw.  Mutators that delegate to the entity or
-	 * header sub-beans are frozen by the {@link #freeze()} deep-freeze of those sub-beans; this funnel additionally
-	 * covers the direct-field {@link #setHeaders(HeaderList)} reassignment.
+	 * Each leaf's nested {@code Unmodifiable} snapshot overrides this method to throw.  Mutators that delegate to the
+	 * entity or header sub-beans are frozen by the {@link #freeze()} deep-freeze of those sub-beans; this funnel
+	 * additionally covers the direct-field {@link #setHeaders(HeaderList)} reassignment.
 	 *
 	 * @param mutation The state change to apply.
 	 * @return This object.
 	 */
-	protected BasicResource modify(Runnable mutation) {
+	protected SELF modify(Runnable mutation) {
 		mutation.run();
-		return this;
+		return self();
 	}
 
 	/**
-	 * Deep-freeze hook invoked from the nested {@link Unmodifiable} constructor after the snapshot copy completes.
+	 * Deep-freeze hook invoked from a leaf's nested {@code Unmodifiable} constructor after the snapshot copy completes.
 	 *
 	 * <p>
 	 * The mutable sub-beans are frozen here by <b>direct field assignment</b> (never via a setter, which would route
@@ -473,33 +486,4 @@ public class BasicResource implements HttpResource {
 		entity = entity.unmodifiable();   // DIRECT field write — snapshot the copied entity.
 		headers = headers.unmodifiable(); // DIRECT field write — snapshot the copied HeaderList.
 	}
-
-	/**
-	 * An unmodifiable snapshot of a {@link BasicResource}.
-	 *
-	 * <p>
-	 * Its behavioral override is a throwing {@link #modify(Runnable)}, and its constructor deep-freezes the entity and
-	 * header sub-beans via {@link #freeze()}.  Together these freeze the entire mutation surface: direct-field mutators
-	 * throw through {@code modify(...)}, and sub-bean-delegating mutators throw through the frozen entity/headers.
-	 */
-	public static class Unmodifiable extends BasicResource implements UnmodifiableBean {
-
-		/**
-		 * Constructor.
-		 *
-		 * @param copyFrom The bean to snapshot-copy.  Must not be <jk>null</jk>.
-		 */
-		@SuppressWarnings({
-			"java:S1699" // Paradigm intentionally calls the overridable freeze() from the ctor to deep-freeze sub-beans.
-		})
-		protected Unmodifiable(BasicResource copyFrom) {
-			super(copyFrom);
-			freeze();
-		}
-
-		@Override /* Overridden from BasicResource */
-		protected BasicResource modify(Runnable mutation) {
-			throw uoex("Bean is unmodifiable.");
-		}
-	}
 }
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ByteArrayResource.java b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ByteArrayResource.java
index 238b69e63c..b5c8935569 100644
--- a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ByteArrayResource.java
+++ b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ByteArrayResource.java
@@ -18,10 +18,6 @@ package org.apache.juneau.http.classic.resource;
 
 import static org.apache.juneau.commons.utils.Shorts.*;
 
-import java.io.*;
-import java.util.function.*;
-
-import org.apache.http.*;
 import org.apache.juneau.http.UnmodifiableBean;
 import org.apache.juneau.http.classic.entity.*;
 import org.apache.juneau.http.classic.header.*;
@@ -33,7 +29,7 @@ import org.apache.juneau.http.classic.header.*;
  * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common Basics</a>
  * </ul>
  */
-public class ByteArrayResource extends BasicResource {
+public class ByteArrayResource extends BasicResource<ByteArrayResource> {
 
 	/**
 	 * Constructor.
@@ -62,100 +58,10 @@ public class ByteArrayResource extends BasicResource {
 	}
 
 	@Override /* Overridden from BasicResource */
-	public ByteArrayResource addHeader(String name, String value) {
-		super.addHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource addHeaders(Header...values) {
-		super.addHeaders(values);
-		return this;
-	}
-
-	@Override
 	public ByteArrayResource copy() {
 		return new ByteArrayResource(this);
 	}
 
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setCached() throws IOException {
-		super.setCached();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setChunked() {
-		super.setChunked();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setChunked(boolean value) {
-		super.setChunked(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContent(Object value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContent(Supplier<?> value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContentEncoding(ContentEncoding value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContentEncoding(String value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContentLength(long value) {
-		super.setContentLength(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContentType(ContentType value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setContentType(String value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setHeader(String name, String value) {
-		super.setHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setHeaders(Header...values) {
-		super.setHeaders(values);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ByteArrayResource setHeaders(HeaderList value) {
-		super.setHeaders(value);
-		return this;
-	}
-
 	@Override /* Overridden from BasicResource */
 	public ByteArrayResource unmodifiable() {
 		return this instanceof UnmodifiableBean ? this : new Unmodifiable(this);
@@ -180,7 +86,7 @@ public class ByteArrayResource extends BasicResource {
 		}
 
 		@Override /* Overridden from BasicResource */
-		protected BasicResource modify(Runnable mutation) {
+		protected ByteArrayResource modify(Runnable mutation) {
 			throw uoex("Bean is unmodifiable.");
 		}
 	}
diff --git a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/FileResource.java b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/FileResource.java
index c4eb9d9ddd..7920b55fe4 100644
--- a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/FileResource.java
+++ b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/FileResource.java
@@ -19,9 +19,6 @@ package org.apache.juneau.http.classic.resource;
 import static org.apache.juneau.commons.utils.Shorts.*;
 
 import java.io.*;
-import java.util.function.*;
-
-import org.apache.http.*;
 import org.apache.juneau.http.UnmodifiableBean;
 import org.apache.juneau.http.classic.entity.*;
 import org.apache.juneau.http.classic.header.*;
@@ -33,7 +30,7 @@ import org.apache.juneau.http.classic.header.*;
  * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common Basics</a>
  * </ul>
  */
-public class FileResource extends BasicResource {
+public class FileResource extends BasicResource<FileResource> {
 
 	/**
 	 * Constructor.
@@ -62,100 +59,10 @@ public class FileResource extends BasicResource {
 	}
 
 	@Override /* Overridden from BasicResource */
-	public FileResource addHeader(String name, String value) {
-		super.addHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource addHeaders(Header...values) {
-		super.addHeaders(values);
-		return this;
-	}
-
-	@Override
 	public FileResource copy() {
 		return new FileResource(this);
 	}
 
-	@Override /* Overridden from BasicResource */
-	public FileResource setCached() throws IOException {
-		super.setCached();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setChunked() {
-		super.setChunked();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setChunked(boolean value) {
-		super.setChunked(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContent(Object value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContent(Supplier<?> value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContentEncoding(ContentEncoding value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContentEncoding(String value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContentLength(long value) {
-		super.setContentLength(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContentType(ContentType value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setContentType(String value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setHeader(String name, String value) {
-		super.setHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setHeaders(Header...values) {
-		super.setHeaders(values);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public FileResource setHeaders(HeaderList value) {
-		super.setHeaders(value);
-		return this;
-	}
-
 	@Override /* Overridden from BasicResource */
 	public FileResource unmodifiable() {
 		return this instanceof UnmodifiableBean ? this : new Unmodifiable(this);
@@ -180,7 +87,7 @@ public class FileResource extends BasicResource {
 		}
 
 		@Override /* Overridden from BasicResource */
-		protected BasicResource modify(Runnable mutation) {
+		protected FileResource modify(Runnable mutation) {
 			throw uoex("Bean is unmodifiable.");
 		}
 	}
diff --git a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ReaderResource.java b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ReaderResource.java
index 1fdf757d55..a474eb2b71 100644
--- a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ReaderResource.java
+++ b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/ReaderResource.java
@@ -19,9 +19,6 @@ package org.apache.juneau.http.classic.resource;
 import static org.apache.juneau.commons.utils.Shorts.*;
 
 import java.io.*;
-import java.util.function.*;
-
-import org.apache.http.*;
 import org.apache.juneau.http.UnmodifiableBean;
 import org.apache.juneau.http.classic.entity.*;
 import org.apache.juneau.http.classic.header.*;
@@ -33,7 +30,7 @@ import org.apache.juneau.http.classic.header.*;
  * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common Basics</a>
  * </ul>
  */
-public class ReaderResource extends BasicResource {
+public class ReaderResource extends BasicResource<ReaderResource> {
 
 	/**
 	 * Constructor.
@@ -62,100 +59,10 @@ public class ReaderResource extends BasicResource {
 	}
 
 	@Override /* Overridden from BasicResource */
-	public ReaderResource addHeader(String name, String value) {
-		super.addHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource addHeaders(Header...values) {
-		super.addHeaders(values);
-		return this;
-	}
-
-	@Override
 	public ReaderResource copy() {
 		return new ReaderResource(this);
 	}
 
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setCached() throws IOException {
-		super.setCached();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setChunked() {
-		super.setChunked();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setChunked(boolean value) {
-		super.setChunked(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContent(Object value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContent(Supplier<?> value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContentEncoding(ContentEncoding value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContentEncoding(String value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContentLength(long value) {
-		super.setContentLength(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContentType(ContentType value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setContentType(String value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setHeader(String name, String value) {
-		super.setHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setHeaders(Header...values) {
-		super.setHeaders(values);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public ReaderResource setHeaders(HeaderList value) {
-		super.setHeaders(value);
-		return this;
-	}
-
 	@Override /* Overridden from BasicResource */
 	public ReaderResource unmodifiable() {
 		return this instanceof UnmodifiableBean ? this : new Unmodifiable(this);
@@ -180,7 +87,7 @@ public class ReaderResource extends BasicResource {
 		}
 
 		@Override /* Overridden from BasicResource */
-		protected BasicResource modify(Runnable mutation) {
+		protected ReaderResource modify(Runnable mutation) {
 			throw uoex("Bean is unmodifiable.");
 		}
 	}
diff --git a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StreamResource.java b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StreamResource.java
index b100855bd1..96375ee4d4 100644
--- a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StreamResource.java
+++ b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StreamResource.java
@@ -19,8 +19,6 @@ package org.apache.juneau.http.classic.resource;
 import static org.apache.juneau.commons.utils.Shorts.*;
 
 import java.io.*;
-import java.util.function.*;
-
 import org.apache.http.*;
 import org.apache.juneau.http.UnmodifiableBean;
 import org.apache.juneau.http.classic.entity.*;
@@ -33,7 +31,7 @@ import org.apache.juneau.http.classic.header.*;
  * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common Basics</a>
  * </ul>
  */
-public class StreamResource extends BasicResource {
+public class StreamResource extends BasicResource<StreamResource> {
 
 	/**
 	 * Constructor.
@@ -61,101 +59,24 @@ public class StreamResource extends BasicResource {
 		super(copyFrom);
 	}
 
-	@Override /* Overridden from BasicResource */
-	public StreamResource addHeader(String name, String value) {
-		super.addHeader(name, value);
-		return this;
+	/**
+	 * Constructor.
+	 *
+	 * <p>
+	 * This is the constructor used when converting an HTTP response into a resource.
+	 *
+	 * @param response The HTTP response to copy from.  Must not be <jk>null</jk>.
+	 * @throws IOException Rethrown from {@link HttpEntity#getContent()}.
+	 */
+	public StreamResource(HttpResponse response) throws IOException {
+		super(response);
 	}
 
 	@Override /* Overridden from BasicResource */
-	public StreamResource addHeaders(Header...values) {
-		super.addHeaders(values);
-		return this;
-	}
-
-	@Override
 	public StreamResource copy() {
 		return new StreamResource(this);
 	}
 
-	@Override /* Overridden from BasicResource */
-	public StreamResource setCached() throws IOException {
-		super.setCached();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setChunked() {
-		super.setChunked();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setChunked(boolean value) {
-		super.setChunked(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContent(Object value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContent(Supplier<?> value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContentEncoding(ContentEncoding value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContentEncoding(String value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContentLength(long value) {
-		super.setContentLength(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContentType(ContentType value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setContentType(String value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setHeader(String name, String value) {
-		super.setHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setHeaders(Header...values) {
-		super.setHeaders(values);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StreamResource setHeaders(HeaderList value) {
-		super.setHeaders(value);
-		return this;
-	}
-
 	@Override /* Overridden from BasicResource */
 	public StreamResource unmodifiable() {
 		return this instanceof UnmodifiableBean ? this : new Unmodifiable(this);
@@ -180,7 +101,7 @@ public class StreamResource extends BasicResource {
 		}
 
 		@Override /* Overridden from BasicResource */
-		protected BasicResource modify(Runnable mutation) {
+		protected StreamResource modify(Runnable mutation) {
 			throw uoex("Bean is unmodifiable.");
 		}
 	}
diff --git a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StringResource.java b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StringResource.java
index 93dff6340f..7504238683 100644
--- a/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StringResource.java
+++ b/juneau-rest/juneau-rest-common-classic/src/main/java/org/apache/juneau/http/classic/resource/StringResource.java
@@ -18,10 +18,6 @@ package org.apache.juneau.http.classic.resource;
 
 import static org.apache.juneau.commons.utils.Shorts.*;
 
-import java.io.*;
-import java.util.function.*;
-
-import org.apache.http.*;
 import org.apache.juneau.http.UnmodifiableBean;
 import org.apache.juneau.http.classic.entity.*;
 import org.apache.juneau.http.classic.header.*;
@@ -33,7 +29,7 @@ import org.apache.juneau.http.classic.header.*;
  * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common Basics</a>
  * </ul>
  */
-public class StringResource extends BasicResource {
+public class StringResource extends BasicResource<StringResource> {
 
 	/**
 	 * Constructor.
@@ -62,100 +58,10 @@ public class StringResource extends BasicResource {
 	}
 
 	@Override /* Overridden from BasicResource */
-	public StringResource addHeader(String name, String value) {
-		super.addHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource addHeaders(Header...values) {
-		super.addHeaders(values);
-		return this;
-	}
-
-	@Override
 	public StringResource copy() {
 		return new StringResource(this);
 	}
 
-	@Override /* Overridden from BasicResource */
-	public StringResource setCached() throws IOException {
-		super.setCached();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setChunked() {
-		super.setChunked();
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setChunked(boolean value) {
-		super.setChunked(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContent(Object value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContent(Supplier<?> value) {
-		super.setContent(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContentEncoding(ContentEncoding value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContentEncoding(String value) {
-		super.setContentEncoding(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContentLength(long value) {
-		super.setContentLength(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContentType(ContentType value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setContentType(String value) {
-		super.setContentType(value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setHeader(String name, String value) {
-		super.setHeader(name, value);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setHeaders(Header...values) {
-		super.setHeaders(values);
-		return this;
-	}
-
-	@Override /* Overridden from BasicResource */
-	public StringResource setHeaders(HeaderList value) {
-		super.setHeaders(value);
-		return this;
-	}
-
 	@Override /* Overridden from BasicResource */
 	public StringResource unmodifiable() {
 		return this instanceof UnmodifiableBean ? this : new Unmodifiable(this);
@@ -180,7 +86,7 @@ public class StringResource extends BasicResource {
 		}
 
 		@Override /* Overridden from BasicResource */
-		protected BasicResource modify(Runnable mutation) {
+		protected StringResource modify(Runnable mutation) {
 			throw uoex("Bean is unmodifiable.");
 		}
 	}
diff --git a/juneau-rest/juneau-rest-common-classic/src/test/java/org/apache/juneau/http/classic/resource/BasicResource_Test.java b/juneau-rest/juneau-rest-common-classic/src/test/java/org/apache/juneau/http/classic/resource/BasicResource_Test.java
index f153ef7fc8..0ed9b7b153 100644
--- a/juneau-rest/juneau-rest-common-classic/src/test/java/org/apache/juneau/http/classic/resource/BasicResource_Test.java
+++ b/juneau-rest/juneau-rest-common-classic/src/test/java/org/apache/juneau/http/classic/resource/BasicResource_Test.java
@@ -25,7 +25,6 @@ import java.io.*;
 import org.apache.http.*;
 import org.apache.http.message.*;
 import org.apache.juneau.*;
-import org.apache.juneau.http.classic.entity.*;
 import org.apache.juneau.http.classic.header.*;
 import org.junit.jupiter.api.*;
 
@@ -40,7 +39,7 @@ class BasicResource_Test extends TestBase {
 	//------------------------------------------------------------------------------------------------------------------
 
 	@Test void a01_constructor_entity() throws Exception {
-		var x = new BasicResource(new StringEntity(contentType("text/plain"), "foo"));
+		var x = new StringResource(contentType("text/plain"), "foo");
 		assertEquals("foo", toUtf8(x.getContent()));
 		assertEquals("text/plain", x.getContentType().getValue());
 		assertNotNull(x.getEntity());
@@ -48,9 +47,9 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void a02_constructor_copy() throws Exception {
-		var src = new BasicResource(new StringEntity(contentType("text/plain"), "foo"))
+		var src = new StringResource(contentType("text/plain"), "foo")
 			.setHeader("Foo", "bar");
-		var x = new BasicResource(src);
+		var x = new StringResource(src);
 		assertNotSame(src, x);
 		assertEquals("foo", toUtf8(x.getContent()));
 		assertEquals("text/plain", x.getContentType().getValue());
@@ -64,7 +63,7 @@ class BasicResource_Test extends TestBase {
 		resp.addHeader("Content-Type", "text/plain");
 		resp.addHeader("Content-Encoding", "identity");
 		resp.addHeader("Content-Length", "5");
-		var x = new BasicResource(resp);
+		var x = new StreamResource(resp);
 		assertEquals("hello", toUtf8(x.getContent()));
 		assertEquals("bar", x.getHeaders().getFirst("X-Foo").get().getValue());
 		assertEquals("text/plain", x.getContentType().getValue());
@@ -77,7 +76,7 @@ class BasicResource_Test extends TestBase {
 	//------------------------------------------------------------------------------------------------------------------
 
 	@Test void b01_addHeader_basic() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.addHeader("Foo", "bar")
 			.addHeader("Foo", "baz")
 			.setHeader("Qux", "quux");
@@ -88,7 +87,7 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void b02_addHeader_nullSafe() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.addHeader(null, "bar")
 			.addHeader("Foo", null)
 			.setHeader(null, "x")
@@ -97,7 +96,7 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void b03_addHeaders_specialHeaders() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.addHeaders(
 				header("Content-Type", "text/plain"),
 				header("Content-Encoding", "identity"),
@@ -113,7 +112,7 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void b04_setHeaders_specialHeaders() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.setHeaders(
 				header("Content-Type", "text/plain"),
 				header("Content-Encoding", "identity"),
@@ -130,7 +129,7 @@ class BasicResource_Test extends TestBase {
 
 	@Test void b05_setHeaders_headerList() {
 		var hl = HeaderList.create().append("Foo", "bar");
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.setHeaders(hl);
 		assertEquals("bar", x.getHeaders().getFirst("Foo").get().getValue());
 	}
@@ -140,47 +139,47 @@ class BasicResource_Test extends TestBase {
 	//------------------------------------------------------------------------------------------------------------------
 
 	@Test void c01_setContent_value() throws Exception {
-		var x = new BasicResource(new StringEntity(null, "old"))
+		var x = new StringResource(null, "old")
 			.setContent("new");
 		assertEquals("new", toUtf8(x.getContent()));
 	}
 
 	@Test void c02_setContent_supplier() throws Exception {
-		var x = new BasicResource(new StringEntity(null, null))
+		var x = new StringResource(null, null)
 			.setContent(() -> "newish");
 		assertEquals("newish", toUtf8(x.getContent()));
 	}
 
 	@Test void c03_setContentType_string_andContentType() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.setContentType("text/plain");
 		assertEquals("text/plain", x.getContentType().getValue());
 
-		var x2 = new BasicResource(new StringEntity(null, "foo"))
+		var x2 = new StringResource(null, "foo")
 			.setContentType(contentType("text/html"));
 		assertEquals("text/html", x2.getContentType().getValue());
 	}
 
 	@Test void c04_setContentEncoding_string_andContentEncoding() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.setContentEncoding("identity");
 		assertEquals("identity", x.getContentEncoding().getValue());
 
-		var x2 = new BasicResource(new StringEntity(null, "foo"))
+		var x2 = new StringResource(null, "foo")
 			.setContentEncoding(contentEncoding("gzip"));
 		assertEquals("gzip", x2.getContentEncoding().getValue());
 	}
 
 	@Test void c05_setContentLength() {
-		var x = new BasicResource(new StringEntity(null, "foo"))
+		var x = new StringResource(null, "foo")
 			.setContentLength(10L);
 		assertEquals(10L, x.getContentLength());
 	}
 
 	@Test void c06_chunked() {
-		var x = new BasicResource(new StringEntity(null, "foo")).setChunked();
+		var x = new StringResource(null, "foo").setChunked();
 		assertTrue(x.isChunked());
-		var x2 = new BasicResource(new StringEntity(null, "foo")).setChunked(false);
+		var x2 = new StringResource(null, "foo").setChunked(false);
 		assertFalse(x2.isChunked());
 	}
 
@@ -189,7 +188,7 @@ class BasicResource_Test extends TestBase {
 	//------------------------------------------------------------------------------------------------------------------
 
 	@Test void d01_setCached_andRepeatable() throws Exception {
-		var x = new BasicResource(new StringEntity(null, "foo")).setCached();
+		var x = new StringResource(null, "foo").setCached();
 		assertEquals("foo", toUtf8(x.getContent()));
 		assertEquals("foo", toUtf8(x.getContent()));
 		assertTrue(x.isRepeatable());
@@ -197,19 +196,19 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void d02_asBytes_asString() throws Exception {
-		var x = new BasicResource(new StringEntity(null, "hello"));
+		var x = new StringResource(null, "hello");
 		assertEquals("hello", new String(x.asBytes()));
 		assertEquals("hello", x.asString());
 	}
 
 	@Test void d03_assertBytes_assertString() throws Exception {
-		var x = new BasicResource(new StringEntity(null, "abc"));
+		var x = new StringResource(null, "abc");
 		assertNotNull(x.assertBytes());
 		assertNotNull(x.assertString());
 	}
 
 	@Test void d04_copy_method() throws Exception {
-		var x = new BasicResource(new StringEntity(contentType("text/plain"), "foo"))
+		var x = new StringResource(contentType("text/plain"), "foo")
 			.setHeader("Foo", "bar");
 		var x2 = x.copy();
 		assertNotSame(x, x2);
@@ -219,14 +218,14 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void d05_writeTo() throws Exception {
-		var x = new BasicResource(new StringEntity(null, "writeMe"));
+		var x = new StringResource(null, "writeMe");
 		var out = new ByteArrayOutputStream();
 		x.writeTo(out);
 		assertEquals("writeMe", out.toString("UTF-8"));
 	}
 
 	@Test void d06_consumeContent() {
-		var x = new BasicResource(new StringEntity(null, "foo"));
+		var x = new StringResource(null, "foo");
 		// consumeContent() is a no-op that must complete without throwing.
 		assertDoesNotThrow(x::consumeContent);
 	}
@@ -236,12 +235,12 @@ class BasicResource_Test extends TestBase {
 	//------------------------------------------------------------------------------------------------------------------
 
 	@Test void e01_isUnmodifiable_default() {
-		var x = new BasicResource(new StringEntity(null, "foo"));
+		var x = new StringResource(null, "foo");
 		assertFalse(x.isUnmodifiable());
 	}
 
 	@Test void e02_unmodifiable_returnsFrozenSnapshot() {
-		var x = new BasicResource(new StringEntity(null, "foo"));
+		var x = new StringResource(null, "foo");
 		var u = x.unmodifiable();
 		assertFalse(x.isUnmodifiable());
 		assertTrue(u.isUnmodifiable());
@@ -256,13 +255,13 @@ class BasicResource_Test extends TestBase {
 	}
 
 	@Test void e03_unmodifiable_idempotent() {
-		var u = new BasicResource(new StringEntity(null, "foo")).unmodifiable();
+		var u = new StringResource(null, "foo").unmodifiable();
 		// D1 idempotency: unmodifiable() on an already-unmodifiable snapshot returns the same instance.
 		assertSame(u, u.unmodifiable());
 	}
 
 	@Test void e04_unmodifiable_snapshotIndependence() {
-		var x = new BasicResource(new StringEntity(null, "foo")).addHeader("X-A", "a");
+		var x = new StringResource(null, "foo").addHeader("X-A", "a");
 		var u = x.unmodifiable();
 		var before = u.getHeaders().size();
 		x.addHeader("X-B", "b"); // Mutate original after snapshotting.
@@ -274,7 +273,7 @@ class BasicResource_Test extends TestBase {
 	//------------------------------------------------------------------------------------------------------------------
 
 	@Test void f01_fluentReturns() {
-		var x = new BasicResource(new StringEntity(null, "foo"));
+		var x = new StringResource(null, "foo");
 		assertSame(x, x.addHeader("X-A", "a"));
 		assertSame(x, x.setHeader("X-B", "b"));
 		assertSame(x, x.addHeaders(header("X-C", "c")));