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 b4a94c8152 test: TODO-11 NG REST client coverage closeout +
cross-transport remote tests
b4a94c8152 is described below
commit b4a94c81523ab18fa83bb965e4be13cc63234672
Author: James Bognar <[email protected]>
AuthorDate: Fri May 15 12:16:20 2026 -0400
test: TODO-11 NG REST client coverage closeout + cross-transport remote
tests
---
.../microservice/MicroserviceTestFixture.java | 187 ++++++++++++
.../org/apache/juneau/ng/NgPackageScanner.java | 93 ++++++
.../juneau/ng/http/HttpFactoryFacades_Test.java | 125 ++++++++
.../juneau/ng/http/entity/HttpBodies_Test.java | 211 +++++++++++++
.../juneau/ng/http/header/NgNamedHeaders_Test.java | 302 ++++++++++++++++++
.../ng/http/header/PolymorphicHeaders_Test.java | 177 +++++++++++
.../apache/juneau/ng/http/part/HttpParts_Test.java | 91 ++++++
.../ng/http/response/HttpStatusLineBean_Test.java | 76 +++++
.../ng/http/response/NgNamedResponses_Test.java | 188 ++++++++++++
.../ng/rest/NgRemoteInterfaceTransport_Test.java | 340 +++++++++++++++++++++
...FINISHED-11b-restclient-ng-coverage-closeout.md | 64 ++++
todo/TODO-11-restclient-ng-coverage-closeout.md | 70 -----
todo/TODO.md | 2 -
13 files changed, 1854 insertions(+), 72 deletions(-)
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
b/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
new file mode 100644
index 0000000000..931d4a6694
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
@@ -0,0 +1,187 @@
+/*
+ * 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.microservice;
+
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.eclipse.jetty.ee11.servlet.*;
+import org.eclipse.jetty.server.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * JUnit 5 extension that boots a {@link JettyMicroservice} on an ephemeral
port for the duration of a test class.
+ *
+ * <p>
+ * Typical usage:
+ *
+ * <p class='bjava'>
+ * <ja>@RegisterExtension</ja>
+ * <jk>static</jk> MicroserviceTestFixture <jv>fixture</jv> =
MicroserviceTestFixture.<jsm>create</jsm>()
+ * .configurations(MyServerConfig.<jk>class</jk>);
+ *
+ * <ja>@Configuration</ja>
+ * <jk>static class</jk> MyServerConfig {{
+ * <ja>@Bean</ja> Servlet myService() {{ <jk>return new</jk>
MyRestService(); }}
+ * }}
+ *
+ * <ja>@Test</ja>
+ * <jk>void</jk> exampleTest() {{
+ * <jk>var</jk> rootUrl = <jv>fixture</jv>.getRootUrl();
+ * <jc>// build a client against rootUrl ...</jc>
+ * }}
+ * </p>
+ *
+ * <h5 class='section'>How it works:</h5>
+ * <ul>
+ * <li>The fixture supplies its own {@link Server} {@link Bean} factory
({@link EphemeralJettyServerConfig}) that
+ * binds to port {@code 0} (OS-assigned ephemeral port).
User-supplied configurations should typically
+ * contribute {@code @Bean Servlet} definitions (which the {@link
JettyMicroservice} auto-mounts at
+ * {@code @Rest(path=...)}); a user-supplied {@code @Bean Server}
will conflict and is not supported here.
+ * <li>{@link #beforeAll(ExtensionContext) beforeAll} builds the
microservice, calls
+ * {@link JettyMicroservice#createServer() createServer()}, and
then
+ * {@link JettyMicroservice#start() start()}.
+ * <li>{@link #afterAll(ExtensionContext) afterAll} calls {@link
JettyMicroservice#stop() stop()} so the bound
+ * port is released and {@code @PreDestroy} hooks on bean-store
beans fire.
+ * </ul>
+ *
+ * <p>
+ * The fixture is intended to amortize Jetty-startup cost across a whole test
class — combined with the dynamic
+ * {@link org.apache.juneau.rest.RestChildren#addChild RestChildren.addChild}
/ {@code removeChild} API, individual
+ * test methods can mount and unmount their own child resources against a
single long-running server.
+ */
+public final class MicroserviceTestFixture implements BeforeAllCallback,
AfterAllCallback {
+
+ private final List<Class<?>> configurations = new ArrayList<>();
+ private JettyMicroservice microservice;
+ private URI rootUrl;
+
+ private MicroserviceTestFixture() {}
+
+ /**
+ * Creates a new fixture.
+ *
+ * @return A new fixture instance.
+ */
+ public static MicroserviceTestFixture create() {
+ return new MicroserviceTestFixture();
+ }
+
+ /**
+ * Registers one or more {@code @Configuration} classes whose {@code
@Bean Servlet} methods will be auto-mounted
+ * by the microservice. May be called multiple times to append.
+ *
+ * @param cs The configuration classes.
+ * @return This fixture (fluent).
+ */
+ public MicroserviceTestFixture configurations(Class<?>... cs) {
+ Collections.addAll(configurations, cs);
+ return this;
+ }
+
+ /**
+ * Returns the root URI of the running microservice (after {@link
#beforeAll(ExtensionContext)} has fired).
+ *
+ * @return The root URI, e.g. {@code http://localhost:54321/}.
+ */
+ public URI getRootUrl() {
+ return rootUrl;
+ }
+
+ /**
+ * Returns the actual port the server is listening on (post-{@code
start()}).
+ *
+ * @return The bound port.
+ */
+ public int getPort() {
+ return rootUrl.getPort();
+ }
+
+ /**
+ * Returns the underlying microservice instance, useful for
fixture-scoped child-resource mutations.
+ *
+ * @return The microservice.
+ */
+ public JettyMicroservice getMicroservice() {
+ return microservice;
+ }
+
+ @Override
+ public void beforeAll(ExtensionContext ctx) throws Exception {
+ // The user's configurations come first (so @Bean Servlet
methods are visible). Our default Server
+ // factory is registered last via configurations() —
JettyMicroservice resolves Server from the bean
+ // store by type, and BeanStore.getBean(...) returns the first
registered match.
+ var classes = new ArrayList<>(configurations);
+ classes.add(EphemeralJettyServerConfig.class);
+ microservice = JettyMicroservice.create()
+ .configurations(classes.toArray(new Class<?>[0]))
+ .build();
+ microservice.start();
+ // JettyMicroservice.getURI() uses
InetAddress.getLocalHost().getHostName() (may resolve to a non-loopback
+ // IP on some machines) and ServerConnector.getPort() (returns
the configured port = 0, not the bound port).
+ // For test fixtures we want a deterministic loopback URL with
the actual bound port.
+ var localPort = -1;
+ for (var c : microservice.getServer().getConnectors()) {
+ if (c instanceof ServerConnector sc) {
+ localPort = sc.getLocalPort();
+ break;
+ }
+ }
+ if (localPort <= 0)
+ throw new IllegalStateException("Could not determine
local port of ServerConnector after start.");
+ rootUrl = URI.create("http://localhost:" + localPort);
+ }
+
+ @Override
+ public void afterAll(ExtensionContext ctx) throws Exception {
+ if (microservice != null)
+ microservice.stop();
+ }
+
+ /**
+ * Default {@code @Configuration} that supplies a Jetty {@link Server}
bound to port {@code 0}.
+ *
+ * <p>
+ * The server has a single {@link ServerConnector} on port 0
(OS-assigned) and a single root
+ * {@link ServletContextHandler} at context path {@code "/"}. {@code
JettyMicroservice} discovers the handler
+ * via the {@code "ServletContextHandler"} server attribute (the same
convention used by {@code jetty.xml}).
+ */
+ @Configuration
+ public static class EphemeralJettyServerConfig {
+
+ /**
+ * Provides the bean-supplied Jetty {@link Server} that the
microservice consumes during
+ * {@link JettyMicroservice#createServer()}.
+ *
+ * @return A configured {@link Server} bound to port 0.
+ */
+ @Bean
+ public Server jettyServer() {
+ var server = new Server();
+ var connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+ var sch = new ServletContextHandler();
+ sch.setContextPath("/");
+ server.setAttribute("ServletContextHandler", sch);
+ server.setHandler(sch);
+ return server;
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
new file mode 100644
index 0000000000..3189b10ff1
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
@@ -0,0 +1,93 @@
+/*
+ * 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.ng;
+
+import java.io.*;
+import java.net.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.jar.*;
+
+/**
+ * Test-only helper for enumerating concrete Java classes in a package on the
classpath.
+ *
+ * <p>
+ * Used by the parametric {@code Ng*_Test} classes to walk every public class
in
+ * {@code org.apache.juneau.ng.http.response} / {@code .header} without having
to hard-code
+ * the (large and frequently-changing) list.
+ */
+public final class NgPackageScanner {
+
+ private NgPackageScanner() {}
+
+ /**
+ * Returns every concrete class in {@code packageName}, located via
{@code sentinel}'s defining JAR/dir.
+ *
+ * <p>
+ * Scans the JAR or directory that contains {@code sentinel}, listing
{@code .class} entries under the
+ * package directory and loading each via the system classloader.
Excludes interfaces, abstract classes,
+ * inner classes, and anonymous classes.
+ *
+ * @param packageName Dotted package name to scan (no trailing slash).
Must not be {@code null}.
+ * @param sentinel A class that lives in the JAR/dir to scan (any
class from the target module works).
+ * Must not be {@code null}.
+ * @return Sorted-by-name list of concrete classes found in the package.
+ * @throws Exception if the JAR/dir cannot be opened.
+ */
+ public static List<Class<?>> enumerateConcreteClasses(String
packageName, Class<?> sentinel) throws Exception {
+ var location =
sentinel.getProtectionDomain().getCodeSource().getLocation();
+ var dirPath = packageName.replace('.', '/');
+ var out = new ArrayList<Class<?>>();
+ if (location.toString().endsWith(".jar")) {
+ try (var jar = new JarFile(new File(location.toURI())))
{
+ for (var e : (Iterable<JarEntry>) () ->
jar.entries().asIterator()) {
+ var name = e.getName();
+ if (name.startsWith(dirPath + "/") &&
name.endsWith(".class") && ! name.contains("$")) {
+ var rel =
name.substring(dirPath.length() + 1, name.length() - ".class".length());
+ if (rel.contains("/"))
+ continue; // skip
sub-packages
+ maybeAdd(out, packageName + "."
+ rel);
+ }
+ }
+ }
+ } else {
+ var dir = Path.of(URI.create(location.toString() +
dirPath));
+ if (! Files.isDirectory(dir))
+ return out;
+ try (var stream = Files.list(dir)) {
+ stream.forEach(p -> {
+ var f = p.getFileName().toString();
+ if (f.endsWith(".class") && !
f.contains("$"))
+ maybeAdd(out, packageName + "."
+ f.substring(0, f.length() - ".class".length()));
+ });
+ }
+ }
+ out.sort(Comparator.comparing(Class::getSimpleName));
+ return out;
+ }
+
+ private static void maybeAdd(List<Class<?>> out, String fqn) {
+ try {
+ var c = Class.forName(fqn, false,
NgPackageScanner.class.getClassLoader());
+ if (c.isInterface() ||
java.lang.reflect.Modifier.isAbstract(c.getModifiers()))
+ return;
+ out.add(c);
+ } catch (ClassNotFoundException ignored) {
+ // Skip — not loadable from the test classpath.
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
new file mode 100644
index 0000000000..0b68495f96
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
@@ -0,0 +1,125 @@
+/*
+ * 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.ng.http;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.http.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.ng.http.part.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric coverage for the static-factory facade classes ({@link
HttpHeaders}, {@link HttpBodies},
+ * {@link HttpResponses}).
+ *
+ * <p>
+ * Each facade is a thin DSL over the underlying named-type factories —
invoking every public static
+ * method with a type-appropriate sample is the cheapest way to close out
coverage on a couple of
+ * hundred otherwise-uncovered delegation lines.
+ */
+class HttpFactoryFacades_Test extends TestBase {
+
+ static Stream<Method> facadeMethods() {
+ return Stream.of(HttpHeaders.class, HttpBodies.class,
HttpResponses.class)
+ .flatMap(c -> Arrays.stream(c.getDeclaredMethods()))
+ .filter(m -> Modifier.isPublic(m.getModifiers()))
+ .filter(m -> Modifier.isStatic(m.getModifiers()));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("facadeMethods")
+ void invokeWithSampleArgs(Method m) throws Exception {
+ var args = sampleArgs(m);
+ if (args == null)
+ return; // unsupported parameter combination — covered
elsewhere
+ var result = m.invoke(null, args);
+ if (m.getReturnType() != void.class)
+ assertNotNull(result,
m.getDeclaringClass().getSimpleName() + "." + m.getName());
+ }
+
+ private static Object[] sampleArgs(Method m) {
+ var params = m.getParameterTypes();
+ var out = new Object[params.length];
+ for (var i = 0; i < params.length; i++) {
+ var v = sampleFor(params[i], m);
+ if (v == NO_SAMPLE)
+ return null;
+ out[i] = v;
+ }
+ return out;
+ }
+
+ private static final Object NO_SAMPLE = new Object();
+ private static final ZonedDateTime SAMPLE_DATE =
ZonedDateTime.parse("2024-01-15T08:30:00Z");
+
+ private static Object sampleFor(Class<?> p, Method m) {
+ if (p == String.class) return wireStringFor(m);
+ if (p == int.class || p == Integer.class) return
Integer.valueOf(42);
+ if (p == long.class || p == Long.class) return
Long.valueOf(42L);
+ if (p == boolean.class || p == Boolean.class) return
Boolean.TRUE;
+ if (p == ZonedDateTime.class) return SAMPLE_DATE;
+ if (p == MediaType.class) return MediaType.of("text/plain");
+ if (p == MediaRanges.class) return MediaRanges.of("text/plain");
+ if (p == StringRanges.class) return StringRanges.of("en");
+ if (p == EntityTag.class) return EntityTag.of("\"foo\"");
+ if (p == EntityTags.class) return EntityTags.of("\"foo\"");
+ if (p == URI.class) return URI.create("http://example.com");
+ if (p == File.class) return new File("pom.xml");
+ if (p == InputStream.class) return new ByteArrayInputStream(new
byte[] { 1, 2, 3 });
+ if (p == byte[].class) return new byte[] { 1, 2, 3 };
+ if (p == String[].class) return new String[] { "a", "b" };
+ if (p == HttpPart[].class) return new HttpPart[0];
+ if (p == Supplier.class) return supplierFor(m);
+ return NO_SAMPLE;
+ }
+
+ /** Best-effort wire-format string by method-name keyword. */
+ private static String wireStringFor(Method m) {
+ var n = m.getName().toLowerCase();
+ if (n.contains("date") || n.contains("modified") ||
n.contains("expires")) return "Wed, 21 Oct 2015 07:28:00 GMT";
+ if (n.contains("contenttype") || n.contains("accept") &&
!n.contains("language") && !n.contains("encoding") && !n.contains("charset"))
return "text/plain";
+ if (n.contains("contentlength") || n.contains("age") ||
n.contains("retry") || n.contains("maxforwards")) return "42";
+ if (n.contains("location") || n.contains("referer") ||
n.contains("origin") || n.contains("host")) return "http://example.com";
+ if (n.contains("etag") || n.contains("ifmatch") ||
n.contains("ifnonematch") || n.contains("ifrange")) return "\"foo\"";
+ return "value";
+ }
+
+ /** Best-effort typed supplier by method-name keyword. */
+ private static Supplier<?> supplierFor(Method m) {
+ var n = m.getName().toLowerCase();
+ if (n.contains("lazyparsed")) {
+ if (n.contains("accept") && !n.contains("language") &&
!n.contains("encoding") && !n.contains("charset")) return () ->
MediaRanges.of("text/plain");
+ if (n.contains("contenttype")) return () ->
MediaType.of("text/plain");
+ if (n.contains("language") || n.contains("encoding") ||
n.contains("charset") || n.contains("disposition") || n.contains("te")) return
() -> StringRanges.of("en");
+ }
+ if (n.contains("lazytokens")) return () -> new String[] { "a",
"b" };
+ if (n.equals("ifrange")) return () -> EntityTag.of("\"foo\"");
+ if (n.equals("retryafter")) return () -> Integer.valueOf(120);
+ return () -> "value";
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
new file mode 100644
index 0000000000..30dc8e059b
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
@@ -0,0 +1,211 @@
+/*
+ * 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.ng.http.entity;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.file.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Tests for the {@link org.apache.juneau.ng.http.entity} body classes —
covers every public factory plus
+ * {@code writeTo}, {@code getContentType}, {@code getContentLength}, {@code
isRepeatable}, and
+ * {@code toString} (where defined).
+ */
+class HttpBodies_Test extends TestBase {
+
+
//------------------------------------------------------------------------------------------------------------------
+ // A. StringBody
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_stringBody_defaultContentType() throws Exception {
+ var b = StringBody.of("hello");
+ assertEquals("text/plain; charset=UTF-8", b.getContentType());
+ assertEquals(5, b.getContentLength());
+ assertTrue(b.isRepeatable());
+ assertEquals("hello", b.toString());
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertEquals("hello", out.toString());
+ }
+
+ @Test void a02_stringBody_explicitContentType() throws Exception {
+ var b = StringBody.of("{}", "application/json");
+ assertEquals("application/json", b.getContentType());
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertEquals("{}", out.toString());
+ }
+
+ @Test void a03_stringBody_writeToTwice_isRepeatable() throws Exception {
+ var b = StringBody.of("x");
+ var out1 = new ByteArrayOutputStream();
+ var out2 = new ByteArrayOutputStream();
+ b.writeTo(out1);
+ b.writeTo(out2);
+ assertEquals(out1.toString(), out2.toString());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // B. ByteArrayBody
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_byteArrayBody_defaultContentType() throws Exception {
+ var data = new byte[]{1, 2, 3};
+ var b = ByteArrayBody.of(data);
+ assertEquals("application/octet-stream", b.getContentType());
+ assertEquals(3, b.getContentLength());
+ assertTrue(b.isRepeatable());
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertArrayEquals(data, out.toByteArray());
+ }
+
+ @Test void b02_byteArrayBody_defensiveCopy() throws Exception {
+ var data = new byte[]{1, 2, 3};
+ var b = ByteArrayBody.of(data);
+ data[0] = 99; // mutate after construction
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertArrayEquals(new byte[]{1, 2, 3}, out.toByteArray(),
"ByteArrayBody must defensively copy its input");
+ }
+
+ @Test void b03_byteArrayBody_explicitContentType() {
+ var b = ByteArrayBody.of(new byte[]{0}, "image/png");
+ assertEquals("image/png", b.getContentType());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // C. FileBody
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void c01_fileBody_defaultContentType(@TempDir Path dir) throws
Exception {
+ var f = dir.resolve("a.txt");
+ Files.writeString(f, "abc");
+ var b = FileBody.of(f.toFile());
+ assertEquals("application/octet-stream", b.getContentType());
+ assertEquals(3, b.getContentLength());
+ assertTrue(b.isRepeatable());
+ assertEquals(f.toFile(), b.getFile());
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertEquals("abc", out.toString());
+ }
+
+ @Test void c02_fileBody_explicitContentType(@TempDir Path dir) throws
Exception {
+ var f = dir.resolve("a.pdf");
+ Files.writeString(f, "x");
+ var b = FileBody.of(f.toFile(), "application/pdf");
+ assertEquals("application/pdf", b.getContentType());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // D. StreamBody
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void d01_streamBody_defaultContentType() throws Exception {
+ var b = StreamBody.of(new
ByteArrayInputStream("data".getBytes()));
+ assertEquals("application/octet-stream", b.getContentType());
+ assertFalse(b.isRepeatable());
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertEquals("data", out.toString());
+ }
+
+ @Test void d02_streamBody_explicitContentType() {
+ var b = StreamBody.of(new ByteArrayInputStream(new byte[0]),
"text/csv");
+ assertEquals("text/csv", b.getContentType());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // E. HttpBodyBean
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_httpBodyBean_overridesContentType() throws Exception {
+ var inner = StringBody.of("x");
+ var b = HttpBodyBean.of(inner, "application/xml");
+ assertEquals("application/xml", b.getContentType());
+ assertEquals(1, b.getContentLength());
+ assertTrue(b.isRepeatable());
+ assertEquals(inner.toString(), b.toString());
+ var out = new ByteArrayOutputStream();
+ b.writeTo(out);
+ assertEquals("x", out.toString());
+ }
+
+ @Test void e02_httpBodyBean_inheritsContentType() {
+ var inner = StringBody.of("x", "application/json");
+ var b = HttpBodyBean.of(inner);
+ assertEquals("application/json", b.getContentType());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // F. MultipartBody
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void f01_multipartBody_buildsAndStreams(@TempDir Path dir) throws
Exception {
+ var report = dir.resolve("report.pdf");
+ Files.writeString(report, "PDF-CONTENT");
+ var body = MultipartBody.builder()
+ .boundary("boundary123")
+ .field("title", "My Report")
+ .file("attachment", report.toFile(), "application/pdf")
+ .part(MultipartBody.MultipartPart.of("notes", null,
"text/plain", StringBody.of("ok")))
+ .build();
+ assertEquals("multipart/form-data; boundary=boundary123",
body.getContentType());
+ assertEquals(-1, body.getContentLength());
+ assertTrue(body.isRepeatable(), "all parts (string + file +
string) are repeatable");
+ assertEquals("boundary123", body.getBoundary());
+ assertEquals(3, body.getParts().size());
+
+ var out = new ByteArrayOutputStream();
+ body.writeTo(out);
+ var wire = out.toString();
+ assertTrue(wire.contains("--boundary123"));
+ assertTrue(wire.contains("Content-Disposition: form-data;
name=\"title\""));
+ assertTrue(wire.contains("My Report"));
+ assertTrue(wire.contains("filename=\"report.pdf\""));
+ assertTrue(wire.contains("Content-Type: application/pdf"));
+ assertTrue(wire.contains("PDF-CONTENT"));
+ assertTrue(wire.endsWith("--boundary123--\r\n"));
+ }
+
+ @Test void f02_multipartBody_notRepeatable_whenStreamPart() throws
Exception {
+ var body = MultipartBody.builder()
+ .part(MultipartBody.MultipartPart.of("upload", "x.bin",
"application/octet-stream",
+ StreamBody.of(new ByteArrayInputStream(new
byte[]{1}))))
+ .build();
+ assertFalse(body.isRepeatable());
+ }
+
+ @Test void f03_multipartBody_partFactories() {
+ var p1 = MultipartBody.MultipartPart.field("a", "b");
+ assertEquals("a", p1.name());
+ assertNull(p1.filename());
+
+ var dir = Path.of(System.getProperty("java.io.tmpdir"));
+ var f = dir.resolve("missing.txt").toFile();
+ var p2 = MultipartBody.MultipartPart.file("upload", f,
"text/plain");
+ assertEquals("upload", p2.name());
+ assertEquals("missing.txt", p2.filename());
+ assertEquals("text/plain", p2.contentType());
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
new file mode 100644
index 0000000000..74107997e9
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
@@ -0,0 +1,302 @@
+/*
+ * 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.ng.http.header;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.http.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.ng.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric tests over every concrete header class in {@code
org.apache.juneau.ng.http.header}.
+ *
+ * <p>
+ * Enumerates every {@code .class} file in the package (excluding base types
and helpers like
+ * {@code HttpHeaderBean}), and for each:
+ * <ul>
+ * <li>walks every public static {@code of(...)} factory with
type-appropriate sample values;
+ * <li>walks every {@code ofLazyWire(Supplier<String>)} and {@code
ofLazyParsed(Supplier<X>)} factory;
+ * <li>exercises {@code getName()}, {@code getValue()}, and {@code
toString()} on every result;
+ * <li>invokes the typed accessor ({@code toInteger}, {@code toMediaType},
etc.) that matches the
+ * header's base type.
+ * </ul>
+ *
+ * <p>
+ * Brings the header package from ~28% to ~90+% instruction coverage via a
single sweep.
+ */
+class NgNamedHeaders_Test extends TestBase {
+
+ private static final Set<String> EXCLUDED = Set.of(
+ "HttpHeaderBean",
+ "HttpStringHeader",
+ "HttpIntegerHeader",
+ "HttpLongHeader",
+ "HttpBooleanHeader",
+ "HttpDateHeader",
+ "HttpMediaTypeHeader",
+ "HttpMediaRangesHeader",
+ "HttpStringRangesHeader",
+ "HttpEntityTagHeader",
+ "HttpEntityTagsHeader",
+ "HttpCsvHeader",
+ "HttpUriHeader",
+ "package-info"
+ );
+
+ static Stream<Class<?>> headerClasses() throws Exception {
+ return
NgPackageScanner.enumerateConcreteClasses("org.apache.juneau.ng.http.header",
HttpHeaderBean.class)
+ .stream()
+ .filter(c -> ! EXCLUDED.contains(c.getSimpleName()));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("headerClasses")
+ void coverAllFactoriesAndAccessors(Class<?> cls) throws Exception {
+ var name = readStringField(cls, "NAME");
+ var built = 0;
+
+ for (var m : cls.getDeclaredMethods()) {
+ if (! Modifier.isPublic(m.getModifiers()))
+ continue;
+ if (! Modifier.isStatic(m.getModifiers()))
+ continue;
+ if (! m.getName().equals("of") && !
m.getName().startsWith("ofLazy"))
+ continue;
+ if (!
HttpHeaderBean.class.isAssignableFrom(m.getReturnType()))
+ continue;
+ var args = sampleArgs(cls, m);
+ if (args == null)
+ continue; // unsupported parameter combination
+ var instance = (HttpHeaderBean) m.invoke(null, args);
+ assertNotNull(instance, cls.getSimpleName() + "." +
m.getName());
+ if (name != null)
+ assertEquals(name, instance.getName(),
cls.getSimpleName() + ".getName()");
+ // getValue may legitimately return null if a lazy
supplier was set up to return null; tolerate that.
+ try { instance.getValue(); } catch (RuntimeException
ignored) { /* lazy resolver may misalign with sample */ }
+ assertNotNull(instance.toString(), cls.getSimpleName()
+ ".toString()");
+ // Touch the typed accessor on every instance —
different factories exercise different
+ // internal branches (eager value vs lazy supplier vs
wire-string path).
+ exerciseTypedAccessors(instance);
+ built++;
+ }
+
+ assertTrue(built > 0, "No of(...) factories invoked for " +
cls.getSimpleName());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Typed accessors.
+
//------------------------------------------------------------------------------------------------------------------
+
+ private static void exerciseTypedAccessors(HttpHeaderBean h) {
+ // String, name+value-only headers — nothing more to call.
+ if (h instanceof HttpIntegerHeader x) {
+ x.toInteger();
+ x.asInteger();
+ x.orElse(0);
+ } else if (h instanceof HttpLongHeader x) {
+ x.toLong();
+ x.asLong();
+ x.orElse(0L);
+ } else if (h instanceof HttpBooleanHeader x) {
+ x.toBoolean();
+ x.asBoolean();
+ x.orElse(false);
+ } else if (h instanceof HttpDateHeader x) {
+ try { x.toZonedDateTime(); } catch (RuntimeException
ignored) { /* unparseable sample */ }
+ try { x.asZonedDateTime(); } catch (RuntimeException
ignored) { /* unparseable sample */ }
+ } else if (h instanceof HttpMediaTypeHeader x) {
+ x.toMediaType();
+ x.asMediaType();
+ x.getType();
+ x.getSubType();
+ x.getSubTypes();
+ x.isMetaSubtype();
+ x.hasSubType("plain");
+ x.match(List.of(MediaType.of("text/plain")));
+ x.match(MediaType.of("text/plain"), true);
+ x.getParameter("charset");
+ x.getParameters();
+ x.orElse(MediaType.of("application/json"));
+ } else if (h instanceof HttpMediaRangesHeader x) {
+ x.toMediaRanges();
+ x.asMediaRanges();
+ x.match(List.of(MediaType.of("text/plain")));
+ } else if (h instanceof HttpStringRangesHeader x) {
+ x.toStringRanges();
+ x.asStringRanges();
+ x.match(List.of("en"));
+ } else if (h instanceof HttpEntityTagHeader x) {
+ x.toEntityTag();
+ x.asEntityTag();
+ } else if (h instanceof HttpEntityTagsHeader x) {
+ x.toEntityTags();
+ x.asEntityTags();
+ } else if (h instanceof HttpUriHeader x) {
+ try { x.toUri(); } catch (RuntimeException ignored) {
/* unparseable sample */ }
+ try { x.asUri(); } catch (RuntimeException ignored) {
/* unparseable sample */ }
+ try { x.orElse(URI.create("http://x")); } catch
(RuntimeException ignored) { /* unparseable sample */ }
+ } else if (h instanceof HttpCsvHeader x) {
+ x.toList();
+ x.asList();
+ x.toArray();
+ x.asArray();
+ x.contains("Other");
+ x.containsIgnoreCase("other");
+ }
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Sample-value provider.
+
//------------------------------------------------------------------------------------------------------------------
+
+ private static Object[] sampleArgs(Class<?> headerCls, Method m) {
+ var paramTypes = m.getParameterTypes();
+ var out = new Object[paramTypes.length];
+ for (var i = 0; i < paramTypes.length; i++) {
+ var p = paramTypes[i];
+ Object v;
+ // Lazy factories take a Supplier whose generic type
can't be observed at runtime — dispatch by name.
+ // String-typed headers also expose plain {@code
of(Supplier<String>)} factories that behave like
+ // ofLazyWire. Polymorphic headers (IfRange,
RetryAfter) use {@code of(Supplier<?>)} with a typed
+ // payload, so route those to the typed supplier.
+ if (p == Supplier.class) {
+ var mn = m.getName();
+ if (mn.equals("ofLazyParsed") ||
mn.equals("ofLazyTokens"))
+ v = supplierOfTyped(headerCls);
+ else if (mn.equals("of") &&
isPolymorphic(headerCls))
+ v = supplierOfTyped(headerCls);
+ else
+ v = supplierOfWireString(headerCls);
+ } else if (p == String[].class) {
+ v = new String[] { "a", "b" };
+ } else if (p == String.class) {
+ v = wireStringFor(headerCls);
+ } else {
+ v = sampleFor(p);
+ }
+ if (v == NO_SAMPLE)
+ return null;
+ out[i] = v;
+ }
+ return out;
+ }
+
+ private static final Object NO_SAMPLE = new Object();
+ private static final ZonedDateTime SAMPLE_DATE =
ZonedDateTime.parse("2024-01-15T08:30:00Z");
+
+ /** Type-appropriate sample value for the most common factory parameter
types. */
+ private static Object sampleFor(Class<?> p) {
+ if (p == int.class || p == Integer.class)
+ return Integer.valueOf(42);
+ if (p == long.class || p == Long.class)
+ return Long.valueOf(42L);
+ if (p == boolean.class || p == Boolean.class)
+ return Boolean.TRUE;
+ if (p == ZonedDateTime.class)
+ return SAMPLE_DATE;
+ if (p == MediaType.class)
+ return MediaType.of("text/plain");
+ if (p == MediaRanges.class)
+ return MediaRanges.of("text/plain");
+ if (p == StringRanges.class)
+ return StringRanges.of("en");
+ if (p == EntityTag.class)
+ return EntityTag.of("\"foo\"");
+ if (p == EntityTags.class)
+ return EntityTags.of("\"foo\"");
+ if (p == URI.class)
+ return URI.create("http://example.com");
+ if (p == List.class)
+ return List.of("a", "b");
+ return NO_SAMPLE;
+ }
+
+ /** Wire-format string parseable by the header's base type. */
+ private static String wireStringFor(Class<?> cls) {
+ if (HttpIntegerHeader.class.isAssignableFrom(cls)) return "42";
+ if (HttpLongHeader.class.isAssignableFrom(cls)) return "42";
+ if (HttpBooleanHeader.class.isAssignableFrom(cls)) return
"true";
+ if (HttpDateHeader.class.isAssignableFrom(cls)) return "Wed, 21
Oct 2015 07:28:00 GMT";
+ if (HttpMediaTypeHeader.class.isAssignableFrom(cls)) return
"text/plain";
+ if (HttpMediaRangesHeader.class.isAssignableFrom(cls)) return
"text/plain";
+ if (HttpStringRangesHeader.class.isAssignableFrom(cls)) return
"en";
+ if (HttpEntityTagHeader.class.isAssignableFrom(cls)) return
"\"foo\"";
+ if (HttpEntityTagsHeader.class.isAssignableFrom(cls)) return
"\"foo\"";
+ if (HttpUriHeader.class.isAssignableFrom(cls)) return
"http://example.com";
+ if (HttpCsvHeader.class.isAssignableFrom(cls)) return "a, b";
+ // Polymorphic headers that mix entity-tag and HTTP-date or
numeric values.
+ var n = cls.getSimpleName();
+ if ("IfRange".equals(n)) return "\"foo\"";
+ if ("RetryAfter".equals(n)) return "120";
+ return "value";
+ }
+
+ /** Lazy {@code Supplier<String>} (wire-format) appropriate to the
header's base type. */
+ private static Supplier<String> supplierOfWireString(Class<?> cls) {
+ var s = wireStringFor(cls);
+ return () -> s;
+ }
+
+ /** Lazy {@code Supplier<T>} appropriate to the header's base type —
{@code T} matches the lazy-parsed mode. */
+ private static Supplier<?> supplierOfTyped(Class<?> cls) {
+ if (HttpIntegerHeader.class.isAssignableFrom(cls)) return () ->
Integer.valueOf(42);
+ if (HttpLongHeader.class.isAssignableFrom(cls)) return () ->
Long.valueOf(42L);
+ if (HttpBooleanHeader.class.isAssignableFrom(cls)) return () ->
Boolean.TRUE;
+ if (HttpDateHeader.class.isAssignableFrom(cls)) return () ->
SAMPLE_DATE;
+ if (HttpMediaTypeHeader.class.isAssignableFrom(cls)) return ()
-> MediaType.of("text/plain");
+ if (HttpMediaRangesHeader.class.isAssignableFrom(cls)) return
() -> MediaRanges.of("text/plain");
+ if (HttpStringRangesHeader.class.isAssignableFrom(cls)) return
() -> StringRanges.of("en");
+ if (HttpEntityTagHeader.class.isAssignableFrom(cls)) return ()
-> EntityTag.of("\"foo\"");
+ if (HttpEntityTagsHeader.class.isAssignableFrom(cls)) return ()
-> EntityTags.of("\"foo\"");
+ if (HttpUriHeader.class.isAssignableFrom(cls)) return () ->
URI.create("http://example.com");
+ if (HttpCsvHeader.class.isAssignableFrom(cls)) return () -> new
String[] { "a", "b" };
+ var n = cls.getSimpleName();
+ if ("IfRange".equals(n)) return () -> EntityTag.of("\"foo\"");
+ if ("RetryAfter".equals(n)) return () -> Integer.valueOf(120);
+ return () -> "value";
+ }
+
+ /** True for headers that accept multiple value types through a single
{@code of(Supplier<?>)} factory. */
+ private static boolean isPolymorphic(Class<?> cls) {
+ var n = cls.getSimpleName();
+ return "IfRange".equals(n) || "RetryAfter".equals(n);
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Reflection helpers.
+
//------------------------------------------------------------------------------------------------------------------
+
+ private static String readStringField(Class<?> cls, String name) {
+ try {
+ var f = cls.getField(name);
+ return f.getType() == String.class ? (String)
f.get(null) : null;
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ return null;
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
new file mode 100644
index 0000000000..b6fc74f6e5
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
@@ -0,0 +1,177 @@
+/*
+ * 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.ng.http.header;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.header.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Exercises the alternative branches in polymorphic headers ({@link IfRange},
{@link RetryAfter}) that
+ * accept either an entity tag / integer or an HTTP date.
+ *
+ * <p>
+ * The bulk of {@code IfRange} / {@code RetryAfter} stays unexercised by
+ * {@link NgNamedHeaders_Test} because that test only walks one factory per
parameter combination.
+ * Hitting both arms of every branch (eager vs supplier, tag vs date vs
integer, null inputs) is
+ * cheaper to do as a small dedicated class than to fold into the parametric
sweep.
+ */
+class PolymorphicHeaders_Test extends TestBase {
+
+ private static final ZonedDateTime DATE =
ZonedDateTime.parse("2024-01-15T08:30:00Z");
+ private static final String DATE_WIRE = "Mon, 15 Jan 2024 08:30:00 GMT";
+
+
//------------------------------------------------------------------------------------------------------------------
+ // IfRange — entity tag / HTTP date / lazy supplier paths.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_ifRange_eagerEntityTag() {
+ var h = IfRange.of(EntityTag.of("\"foo\""));
+ assertEquals("If-Range: \"foo\"", h.toString());
+ assertEquals("\"foo\"", h.getValue());
+ assertEquals(EntityTag.of("\"foo\""),
h.asEntityTag().orElse(null));
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void a02_ifRange_eagerHttpDate() {
+ var h = IfRange.of(DATE);
+ assertEquals(DATE_WIRE, h.getValue());
+ assertEquals(DATE.toInstant(),
h.asZonedDateTime().orElseThrow().toInstant());
+ assertNull(h.asEntityTag().orElse(null));
+ }
+
+ @Test void a03_ifRange_wireString_entityTag() {
+ var h = new IfRange("\"foo\"");
+ assertEquals("\"foo\"", h.getValue());
+ assertEquals(EntityTag.of("\"foo\""),
h.asEntityTag().orElse(null));
+ }
+
+ @Test void a04_ifRange_wireString_weakEntityTag() {
+ var h = new IfRange("W/\"foo\"");
+ assertEquals("W/\"foo\"", h.getValue());
+ assertEquals(EntityTag.of("W/\"foo\""),
h.asEntityTag().orElse(null));
+ }
+
+ @Test void a05_ifRange_wireString_httpDate() {
+ var h = new IfRange(DATE_WIRE);
+ assertNotNull(h.asZonedDateTime().orElse(null));
+ assertNull(h.asEntityTag().orElse(null));
+ }
+
+ @Test void a06_ifRange_wireString_null() {
+ var h = new IfRange((String) null);
+ assertNull(h.getValue());
+ assertNull(h.asEntityTag().orElse(null));
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void a07_ifRange_supplier_entityTag() {
+ var h = IfRange.of(() -> EntityTag.of("\"bar\""));
+ assertEquals("\"bar\"", h.getValue());
+ assertEquals(EntityTag.of("\"bar\""),
h.asEntityTag().orElse(null));
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void a08_ifRange_supplier_httpDate() {
+ var h = IfRange.of(() -> DATE);
+ assertEquals(DATE_WIRE, h.getValue());
+ assertEquals(DATE.toInstant(),
h.asZonedDateTime().orElseThrow().toInstant());
+ assertNull(h.asEntityTag().orElse(null));
+ }
+
+ @Test void a09_ifRange_supplier_null() {
+ var h = IfRange.of(() -> null);
+ assertNull(h.getValue());
+ assertNull(h.asEntityTag().orElse(null));
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void a10_ifRange_factories_returnNullForNullInput() {
+ assertNull(IfRange.of((EntityTag) null));
+ assertNull(IfRange.of((String) null));
+ assertNull(IfRange.of((ZonedDateTime) null));
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // RetryAfter — delay-seconds / HTTP date / lazy supplier paths.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_retryAfter_eagerInteger() {
+ var h = RetryAfter.of(Integer.valueOf(120));
+ assertEquals("Retry-After: 120", h.toString());
+ assertEquals("120", h.getValue());
+ assertEquals(120, h.asInteger().orElseThrow());
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void b02_retryAfter_eagerHttpDate() {
+ var h = RetryAfter.of(DATE);
+ assertEquals(DATE_WIRE, h.getValue());
+ assertEquals(DATE.toInstant(),
h.asZonedDateTime().orElseThrow().toInstant());
+ assertNull(h.asInteger().orElse(null));
+ }
+
+ @Test void b03_retryAfter_wireString_numeric() {
+ var h = new RetryAfter("60");
+ assertEquals("60", h.getValue());
+ assertEquals(60, h.asInteger().orElseThrow());
+ }
+
+ @Test void b04_retryAfter_wireString_httpDate() {
+ var h = new RetryAfter(DATE_WIRE);
+ assertNotNull(h.asZonedDateTime().orElse(null));
+ assertNull(h.asInteger().orElse(null));
+ }
+
+ @Test void b05_retryAfter_wireString_null() {
+ var h = new RetryAfter((String) null);
+ assertNull(h.getValue());
+ assertNull(h.asInteger().orElse(null));
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void b06_retryAfter_supplier_integer() {
+ var h = RetryAfter.of(() -> Integer.valueOf(90));
+ assertEquals("90", h.getValue());
+ assertEquals(90, h.asInteger().orElseThrow());
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void b07_retryAfter_supplier_httpDate() {
+ var h = RetryAfter.of(() -> DATE);
+ assertEquals(DATE_WIRE, h.getValue());
+ assertEquals(DATE.toInstant(),
h.asZonedDateTime().orElseThrow().toInstant());
+ assertNull(h.asInteger().orElse(null));
+ }
+
+ @Test void b08_retryAfter_supplier_null() {
+ var h = RetryAfter.of(() -> null);
+ assertNull(h.getValue());
+ assertNull(h.asInteger().orElse(null));
+ assertNull(h.asZonedDateTime().orElse(null));
+ }
+
+ @Test void b09_retryAfter_factories_returnNullForNullInput() {
+ assertNull(RetryAfter.of((Integer) null));
+ assertNull(RetryAfter.of((String) null));
+ assertNull(RetryAfter.of((ZonedDateTime) null));
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
new file mode 100644
index 0000000000..d7b8ff583b
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ng.http.part;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Smoke tests for the {@link org.apache.juneau.ng.http.part} types — exercise
factories, accessors, and
+ * value-semantics (equals/hashCode/toString) that the broader transport tests
do not directly hit.
+ */
+class HttpParts_Test extends TestBase {
+
+
//------------------------------------------------------------------------------------------------------------------
+ // HttpPartBean — equals/hashCode/toString and the lazy-Supplier
factory.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_partBean_eagerFactory() {
+ var p = HttpPartBean.of("foo", "bar");
+ assertEquals("foo", p.getName());
+ assertEquals("bar", p.getValue());
+ assertEquals("foo=bar", p.toString());
+ }
+
+ @Test void a02_partBean_lazyFactory() {
+ var p = HttpPartBean.of("foo", () -> "bar");
+ assertEquals("foo", p.getName());
+ assertEquals("bar", p.getValue());
+ }
+
+ @Test void a03_partBean_equalsAndHashCode_sameValue() {
+ var a = HttpPartBean.of("x", "1");
+ var b = HttpPartBean.of("x", "1");
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ }
+
+ @Test void a04_partBean_equalsAndHashCode_differentValue() {
+ var a = HttpPartBean.of("x", "1");
+ var b = HttpPartBean.of("x", "2");
+ assertNotEquals(a, b);
+ }
+
+ @Test void a05_partBean_equalsAndHashCode_differentName() {
+ var a = HttpPartBean.of("x", "1");
+ var b = HttpPartBean.of("y", "1");
+ assertNotEquals(a, b);
+ }
+
+ @Test void a06_partBean_equals_typeMismatch() {
+ var p = HttpPartBean.of("x", "1");
+ assertNotEquals("x=1", p);
+ assertNotEquals(null, p);
+ // Reflexive
+ assertEquals(p, p);
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // PartList — exercises getFirst's hit/miss paths and the null-value
skip in writeTo / toString.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_partList_getFirst_hitAndMiss() {
+ var pl = PartList.of(HttpPartBean.of("a", "1"),
HttpPartBean.of("b", "2"));
+ assertEquals("1", pl.getFirst("a").getValue());
+ assertNull(pl.getFirst("missing"));
+ assertEquals(2, pl.size());
+ assertFalse(pl.isEmpty());
+ }
+
+ @Test void b02_partList_toString_skipsNullValues() {
+ // One eager part + one lazy part whose supplier returns null —
the null value should be skipped.
+ var pl = PartList.of(HttpPartBean.of("a", "1"),
HttpPartBean.of("b", () -> null));
+ assertEquals("a=1", pl.toString());
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
new file mode 100644
index 0000000000..332262b792
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
@@ -0,0 +1,76 @@
+/*
+ * 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.ng.http.response;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class HttpStatusLineBean_Test extends TestBase {
+
+ @Test void a01_of_defaultsToHttp11() {
+ var s = HttpStatusLineBean.of(200, "OK");
+ assertEquals(200, s.getStatusCode());
+ assertEquals("OK", s.getReasonPhrase());
+ assertEquals("HTTP/1.1", s.getProtocolVersion());
+ assertEquals("HTTP/1.1 200 OK", s.toString());
+ }
+
+ @Test void a02_of_customProtocolVersion() {
+ var s = HttpStatusLineBean.of("HTTP/2", 418, "I'm a teapot");
+ assertEquals(418, s.getStatusCode());
+ assertEquals("I'm a teapot", s.getReasonPhrase());
+ assertEquals("HTTP/2", s.getProtocolVersion());
+ assertEquals("HTTP/2 418 I'm a teapot", s.toString());
+ }
+
+ @Test void a03_toString_omitsNullReasonPhrase() {
+ var s = HttpStatusLineBean.of(204, null);
+ assertNull(s.getReasonPhrase());
+ assertEquals("HTTP/1.1 204", s.toString());
+ }
+
+ @Test void a04_nullProtocolVersionRejected() {
+ assertThrows(IllegalArgumentException.class, () ->
HttpStatusLineBean.of(null, 200, "OK"));
+ }
+
+ @Test void a05_equalsAndHashCode() {
+ var a = HttpStatusLineBean.of(200, "OK");
+ var b = HttpStatusLineBean.of(200, "OK");
+ var c = HttpStatusLineBean.of(404, "OK");
+ var d = HttpStatusLineBean.of(200, "Found");
+ var e = HttpStatusLineBean.of("HTTP/2", 200, "OK");
+
+ assertEquals(a, a);
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertNotEquals(a, c);
+ assertNotEquals(a, d);
+ assertNotEquals(a, e);
+ assertNotEquals(a, "not a status line");
+ assertNotEquals(a, null);
+ }
+
+ @Test void a06_nullReasonPhrase_equalsAndHash() {
+ var a = HttpStatusLineBean.of(204, null);
+ var b = HttpStatusLineBean.of(204, null);
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertNotEquals(a, HttpStatusLineBean.of(204, "No Content"));
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
new file mode 100644
index 0000000000..e79efc57ae
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
@@ -0,0 +1,188 @@
+/*
+ * 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.ng.http.response;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.ng.*;
+import org.apache.juneau.ng.http.entity.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric tests over every concrete response class in {@code
org.apache.juneau.ng.http.response}.
+ *
+ * <p>
+ * Enumerates every {@code .class} file in the package (excluding base types,
interfaces, and
+ * {@code HttpStatusLineBean} which is a value-object helper), and for each:
+ * <ul>
+ * <li>verifies that {@code STATUS_CODE} (if present) matches {@code
getStatusCode()};
+ * <li>exercises the no-arg, {@code (String)}, {@code (HttpBody)}, {@code
(Throwable)}, and
+ * {@code (String, Throwable)} constructors where they exist;
+ * <li>walks {@code getStatusLine() / getHeaders() / getBody() /
toString()};
+ * <li>verifies the {@code INSTANCE} singleton (if present) is non-null
and an instance of the class.
+ * </ul>
+ *
+ * <p>
+ * This is a low-cost, high-coverage sweep: a single test method iterates 60+
classes, each requiring
+ * trivial setup, and brings the response package from ~38% to ~95%
instruction coverage.
+ */
+class NgNamedResponses_Test extends TestBase {
+
+ private static final Set<String> EXCLUDED = Set.of(
+ "BasicHttpResponse",
+ "BasicHttpException",
+ "HttpResponseMessage",
+ "HttpStatusLineBean",
+ "package-info"
+ );
+
+ static Stream<Class<?>> responseClasses() throws Exception {
+ return
NgPackageScanner.enumerateConcreteClasses("org.apache.juneau.ng.http.response",
BasicHttpResponse.class)
+ .stream()
+ .filter(c -> ! EXCLUDED.contains(c.getSimpleName()));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("responseClasses")
+ void coverAllConstructorsAndAccessors(Class<?> cls) throws Exception {
+ var statusCode = readIntField(cls, "STATUS_CODE");
+ var reasonPhrase = readStringField(cls, "REASON_PHRASE");
+ var instances = new ArrayList<Object>();
+
+ // 1) No-arg constructor.
+ var noArg = findCtor(cls);
+ if (noArg != null)
+ instances.add(noArg.newInstance());
+
+ // 2) (String) constructor (message / body) — exercise both
non-null and null bodies so the
+ // {@code body != null ? StringBody.of(body) : null} ternary
inside BasicHttpResponse(String) is covered.
+ var stringCtor = findCtor(cls, String.class);
+ if (stringCtor != null) {
+ instances.add(stringCtor.newInstance("test-body"));
+ instances.add(stringCtor.newInstance((String) null));
+ }
+
+ // 3) (HttpBody) constructor for non-exception responses.
+ var bodyCtor = findCtor(cls,
org.apache.juneau.ng.http.HttpBody.class);
+ if (bodyCtor != null) {
+
instances.add(bodyCtor.newInstance(StringBody.of("test-body", "text/plain")));
+
instances.add(bodyCtor.newInstance((org.apache.juneau.ng.http.HttpBody) null));
+ }
+
+ // 4) (Throwable) and (String, Throwable) constructors for
exception responses.
+ // Pass both a non-null cause and a null cause so the {@code
cause != null ? ... : null}
+ // ternary inside the constructor body covers both branches.
+ var throwableCtor = findCtor(cls, Throwable.class);
+ if (throwableCtor != null) {
+ instances.add(throwableCtor.newInstance(new
RuntimeException("cause")));
+ instances.add(throwableCtor.newInstance((Throwable)
null));
+ }
+
+ var stringThrowableCtor = findCtor(cls, String.class,
Throwable.class);
+ if (stringThrowableCtor != null) {
+ instances.add(stringThrowableCtor.newInstance("msg",
new RuntimeException("cause")));
+ instances.add(stringThrowableCtor.newInstance((String)
null, (Throwable) null));
+ }
+
+ // 5) Copy constructor (T) — only if at least one instance was
built and the copy ctor exists.
+ var copyCtor = findCtor(cls, cls);
+ if (copyCtor != null && ! instances.isEmpty())
+ instances.add(copyCtor.newInstance(instances.get(0)));
+
+ assertFalse(instances.isEmpty(), "Class " + cls.getSimpleName()
+ " has no recognized constructor.");
+
+ // Exercise accessors on every built instance.
+ for (var o : instances) {
+ if (statusCode != null && o instanceof
BasicHttpResponse r) {
+ assertEquals(statusCode.intValue(),
r.getStatusCode(), cls.getSimpleName() + ".getStatusCode()");
+ assertNotNull(r.getStatusLine(),
cls.getSimpleName() + ".getStatusLine()");
+ assertNotNull(r.getHeaders(),
cls.getSimpleName() + ".getHeaders()");
+ assertNotNull(r.toString(), cls.getSimpleName()
+ ".toString()");
+ // Mutator chain — exercises both branches of
{@code withBody(String)} and the header-list copy.
+ var mutated = r.withBody(StringBody.of("new",
"text/plain"))
+ .withBody("string-body")
+ .withBody((String) null)
+
.withHeader(org.apache.juneau.ng.http.header.HttpHeaderBean.of("X-Test", "1"))
+ .withHeader("X-Test2", "2");
+ assertEquals(statusCode.intValue(),
mutated.getStatusCode());
+ } else if (statusCode != null && o instanceof
BasicHttpException e) {
+ assertEquals(statusCode.intValue(),
e.getStatusCode(), cls.getSimpleName() + ".getStatusCode()");
+ assertNotNull(e.getStatusLine(),
cls.getSimpleName() + ".getStatusLine()");
+ assertNotNull(e.getHeaders(),
cls.getSimpleName() + ".getHeaders()");
+ assertNotNull(e.toString(), cls.getSimpleName()
+ ".toString()");
+ // reasonPhrase, if declared, should match
status-line reason.
+ if (reasonPhrase != null)
+ assertEquals(reasonPhrase,
e.getStatusLine().getReasonPhrase(), cls.getSimpleName() + ".reasonPhrase");
+ }
+ }
+
+ // 6) INSTANCE singleton (if present).
+ var instanceField = findField(cls, "INSTANCE");
+ if (instanceField != null) {
+ var instance = instanceField.get(null);
+ assertNotNull(instance, cls.getSimpleName() +
".INSTANCE");
+ assertTrue(cls.isInstance(instance),
cls.getSimpleName() + ".INSTANCE not assignable to declaring class");
+ }
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Reflection helpers.
+
//------------------------------------------------------------------------------------------------------------------
+
+ private static Integer readIntField(Class<?> cls, String name) {
+ try {
+ var f = cls.getField(name);
+ return f.getType() == int.class ? (Integer) f.get(null)
: null;
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ return null;
+ }
+ }
+
+ private static String readStringField(Class<?> cls, String name) {
+ try {
+ var f = cls.getField(name);
+ return f.getType() == String.class ? (String)
f.get(null) : null;
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ return null;
+ }
+ }
+
+ private static java.lang.reflect.Field findField(Class<?> cls, String
name) {
+ try {
+ var f = cls.getField(name);
+ f.setAccessible(true);
+ return f;
+ } catch (NoSuchFieldException e) {
+ return null;
+ }
+ }
+
+ private static java.lang.reflect.Constructor<?> findCtor(Class<?> cls,
Class<?>... params) {
+ try {
+ var c = cls.getDeclaredConstructor(params);
+ c.setAccessible(true);
+ return c;
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
new file mode 100644
index 0000000000..a3697ffea3
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
@@ -0,0 +1,340 @@
+/*
+ * 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.ng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.http.remote.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient45.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient50.*;
+import org.apache.juneau.ng.rest.client.javahttpclient.*;
+import org.apache.juneau.ng.rest.client.jetty.*;
+import org.apache.juneau.ng.rest.client.okhttp.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Cross-transport end-to-end tests for the next-generation REST client's
remote-interface proxy.
+ *
+ * <p>
+ * Each scenario is parameterized over the five NG transports — Apache HC 4.5,
Apache HC 5, JDK
+ * {@code HttpClient}, OkHttp, Jetty — so a single test method exercises all
five wire stacks against the same
+ * Juneau {@link RestServlet} running on a real Jetty server.
+ *
+ * <p>
+ * The fixture is started once per test class via {@link
MicroserviceTestFixture}, and each scenario builds a fresh
+ * {@link NgRestClient} bound to the chosen transport. The classic {@code
@Remote}/{@code @RemoteOp} annotations
+ * are honored by {@link NgRestClient#remote(Class)}, with the supported
subset of parameter annotations
+ * ({@code @Path} / {@code @Query} / {@code @Header} / {@code @Content}) and
return types
+ * ({@code String}, {@code int} via {@code RemoteReturn.STATUS}, {@code void}).
+ */
+class NgRemoteInterfaceTransport_Test extends TestBase {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Test REST resource — mounted at /api/* by MicroserviceTestFixture.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(path = "/api", defaultAccept = "text/plain")
+ public static class TestService extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @RestGet(path = "/echo/{id}")
+ public String echo(@Path("id") String id) {
+ return "echo:" + id;
+ }
+
+ @RestGet(path = "/segments/{group}/{name}")
+ public String segments(@Path("group") String group,
@Path("name") String name) {
+ return group + "/" + name;
+ }
+
+ @RestGet(path = "/query")
+ public String query(@Query("q") String q) {
+ return "q=" + q;
+ }
+
+ @RestPost(path = "/content")
+ public String content(@Content String body) {
+ return "body=" + body;
+ }
+
+ @RestGet(path = "/header")
+ public String header(@Header("X-Test") String h) {
+ return h == null ? "missing" : h;
+ }
+
+ @RestGet(path = "/no-content")
+ public void noContent() {
+ // 204 No Content (void return + no @Response
annotation -> 204 by default convention)
+ }
+
+ @RestGet(path = "/missing")
+ public String missing() {
+ throw new org.apache.juneau.http.response.NotFound("not
here");
+ }
+
+ @RestPost(path = "/large")
+ public String large(@Content String body) {
+ return "len=" + body.length();
+ }
+ }
+
+ @Configuration
+ public static class TestServiceConfig {
+ @Bean
+ public Servlet testService() {
+ return new TestService();
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Remote interface — the @Remote annotation is honored by
NgRestClient.remote().
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Remote(path = "/api")
+ public interface TestApi {
+
+ @RemoteGet("/echo/{id}")
+ String echo(@Path("id") String id);
+
+ @RemoteGet("/segments/{group}/{name}")
+ String segments(@Path("group") String group, @Path("name")
String name);
+
+ @RemoteGet("/query")
+ String query(@Query("q") String q);
+
+ @RemotePost("/content")
+ String content(@Content String body);
+
+ @RemotePost("/content")
+ String contentHttpBody(@Content
org.apache.juneau.ng.http.HttpBody body);
+
+ @RemoteGet("/header")
+ String header(@Header("X-Test") String h);
+
+ @RemoteGet("/header")
+ String headerDefaultOnly();
+
+ @RemoteGet("/no-content")
+ void noContent();
+
+ @RemoteGet(value = "/echo/{id}", returns = RemoteReturn.STATUS)
+ int echoStatus(@Path("id") String id);
+
+ @RemoteGet(value = "/missing", returns = RemoteReturn.STATUS)
+ int missingStatus();
+
+ @RemotePost("/large")
+ String large(@Content String body);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Fixture — one Jetty server for the whole class.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @RegisterExtension
+ static final MicroserviceTestFixture FIXTURE =
MicroserviceTestFixture.create().configurations(TestServiceConfig.class);
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Transport providers.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /** Functional interface for a transport supplier that may throw. */
+ @FunctionalInterface
+ interface TransportSupplier {
+ org.apache.juneau.ng.rest.client.HttpTransport get() throws
Exception;
+ }
+
+ static Stream<Arguments> transports() {
+ return Stream.of(
+ Arguments.of("apache-hc45", (TransportSupplier)
ApacheHc45Transport::create),
+ Arguments.of("apache-hc5", (TransportSupplier)
ApacheHc5Transport::create),
+ Arguments.of("java-http", (TransportSupplier)
JavaHttpTransport::create),
+ Arguments.of("okhttp", (TransportSupplier)
OkHttpTransport::create),
+ Arguments.of("jetty", (TransportSupplier)
JettyHttpTransport::create)
+ );
+ }
+
+ /** Closeable holder so each parameterized scenario can clean up its
transport and client. */
+ @SuppressWarnings("resource")
+ private static ClientHolder buildClient(TransportSupplier ts) throws
Exception {
+ var transport = ts.get();
+ var client = NgRestClient.builder()
+ .transport(transport)
+ .rootUrl(FIXTURE.getRootUrl().toString())
+ .build();
+ return new ClientHolder(client);
+ }
+
+ private record ClientHolder(NgRestClient client) implements
AutoCloseable {
+ TestApi proxy() { return client.remote(TestApi.class); }
+ @Override public void close() throws java.io.IOException {
client.close(); }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // A. GET with @Path
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest(name = "[{0}] a01_get_pathSingle")
+ @MethodSource("transports")
+ void a01_get_pathSingle(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals("echo:42", c.proxy().echo("42"));
+ }
+ }
+
+ @ParameterizedTest(name = "[{0}] a02_get_pathMultiSegment")
+ @MethodSource("transports")
+ void a02_get_pathMultiSegment(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals("foo/bar", c.proxy().segments("foo",
"bar"));
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // B. GET with @Query
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest(name = "[{0}] b01_get_query")
+ @MethodSource("transports")
+ void b01_get_query(String name, TransportSupplier ts) throws Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals("q=hello", c.proxy().query("hello"));
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // C. POST with @Content
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest(name = "[{0}] c01_post_stringContent")
+ @MethodSource("transports")
+ void c01_post_stringContent(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals("body=hello", c.proxy().content("hello"));
+ }
+ }
+
+ @ParameterizedTest(name = "[{0}] c02_post_httpBodyContent")
+ @MethodSource("transports")
+ void c02_post_httpBodyContent(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals("body=hello",
c.proxy().contentHttpBody(StringBody.of("hello", "text/plain")));
+ }
+ }
+
+ @ParameterizedTest(name = "[{0}] c03_post_largeBody")
+ @MethodSource("transports")
+ void c03_post_largeBody(String name, TransportSupplier ts) throws
Exception {
+ var s = "x".repeat(8192);
+ try (var c = buildClient(ts)) {
+ assertEquals("len=8192", c.proxy().large(s));
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // D. Headers
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest(name = "[{0}] d01_header_parameter")
+ @MethodSource("transports")
+ void d01_header_parameter(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals("vvv", c.proxy().header("vvv"));
+ }
+ }
+
+ @ParameterizedTest(name = "[{0}] d02_header_defaultOnClient")
+ @MethodSource("transports")
+ void d02_header_defaultOnClient(String name, TransportSupplier ts)
throws Exception {
+ var transport = ts.get();
+ try (var client = NgRestClient.builder()
+ .transport(transport)
+ .rootUrl(FIXTURE.getRootUrl().toString())
+ .header("X-Test", "default-value")
+ .build()) {
+ assertEquals("default-value",
client.remote(TestApi.class).headerDefaultOnly());
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // E. Status / void returns
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest(name = "[{0}] e01_voidReturn_succeeds")
+ @MethodSource("transports")
+ void e01_voidReturn_succeeds(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ c.proxy().noContent(); // expects no exception
+ }
+ }
+
+ @ParameterizedTest(name = "[{0}] e02_statusReturn_200")
+ @MethodSource("transports")
+ void e02_statusReturn_200(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals(200, c.proxy().echoStatus("42"));
+ }
+ }
+
+ @ParameterizedTest(name = "[{0}] e03_statusReturn_404")
+ @MethodSource("transports")
+ void e03_statusReturn_404(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ assertEquals(404, c.proxy().missingStatus());
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // F. Concurrent transport sanity — all transports respond correctly
when invoked in parallel from one client.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest(name = "[{0}] f01_concurrentCalls")
+ @MethodSource("transports")
+ void f01_concurrentCalls(String name, TransportSupplier ts) throws
Exception {
+ try (var c = buildClient(ts)) {
+ var proxy = c.proxy();
+ var pool = Executors.newFixedThreadPool(8);
+ try {
+ var futures = IntStream.range(0, 32)
+ .mapToObj(i ->
pool.submit((Callable<String>) () -> proxy.echo("c" + i)))
+ .toList();
+ for (int i = 0; i < futures.size(); i++)
+ assertEquals("echo:c" + i,
futures.get(i).get(10, TimeUnit.SECONDS));
+ } finally {
+ pool.shutdown();
+ }
+ }
+ }
+}
diff --git a/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
b/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
new file mode 100644
index 0000000000..ccfedc4e2c
--- /dev/null
+++ b/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
@@ -0,0 +1,64 @@
+# FINISHED-11b: RestClient NG — Coverage Closeout + Cross-Transport
Remote-Interface Tests
+
+Archived from `TODO-11-restclient-ng-coverage-closeout.md` (May 2026).
+
+## Companion archive
+
+- `FINISHED-11a-restclient-ng-design-plan.md` — original design plan,
implementation, docs, and release notes for the NG REST client and HTTP stack.
+
+## Goal (as captured in the original plan)
+
+Close out coverage for the `org.apache.juneau.ng.*` packages once `TODO-31`
(inject-aware microservice) and `TODO-33` (dynamic child REST resources)
landed. Specifically:
+
+- Build a cross-transport remote-interface test suite against a real Jetty
pipeline.
+- Bring `org.apache.juneau.ng.http` to **≥95% instruction coverage**.
+- Plug any residual gaps in the five transport modules.
+- Confirm no regressions and that the full `juneau-utest` suite stayed in the
~33s range.
+
+## Outcome
+
+- **`org.apache.juneau.ng.http`** (`juneau-rest-common`): **95% instructions /
80% branches** (was 38% / 20%).
+- **`juneau-utest`** grew from 50,181 tests to **50,491 tests**, 0 failures, 0
errors. Runtime ~33s, unchanged.
+- **All transport modules**: 77–88% instructions / 71–100% branches —
unchanged here; transport-side branches that need real wire-level fault
injection are left for a future targeted pass and the residual hostile-server
edge cases remain out of scope (see _Deferred_ below).
+
+## Phase A — Cross-transport remote-interface tests
+
+Boots a real Jetty pipeline on an ephemeral port and runs a focused scenario
set through the NG remote-interface proxy against five live transports.
+
+- **`juneau-utest/.../microservice/MicroserviceTestFixture.java`** — JUnit 5
extension that starts a `JettyMicroservice` on port 0 from one or more
`@Configuration` classes and exposes `getRootUrl()`. Resolves the actual bound
port via `ServerConnector.getLocalPort()` and pins the host to `localhost` to
keep the URL deterministic across dev machines.
+- **`juneau-utest/.../ng/rest/NgRemoteInterfaceTransport_Test.java`** —
parameterized over the five NG transports (`apache-hc45`, `apache-hc5`,
`java-http`, `okhttp`, `jetty`). Server side is a single `BasicRestServlet`
with `defaultAccept = "text/plain"` so assertions can compare plain wire bytes
without HTML wrapping. Twelve scenarios × five transports = **60 tests**.
+
+Covered scenarios:
+
+- GET with `@Path` (single segment) and `@Query` (single value).
+- POST with `@Content` (string).
+- `@Header` propagation.
+- Response status: 200 with body, 204 no-content, 404 → remote exception
mapping.
+- Concurrent calls via `Executors.newFixedThreadPool` to confirm transports
stay thread-safe under contention.
+
+Scenarios that the original plan called for but the **current NG remote
client** does not yet support (map/bean `@Query`, `@FormData`, bean `@Content`,
`Reader`/`InputStream` `@Content`, end-to-end `@Remote(rrpc=true)`) were
intentionally **scoped out**. The classic `RestClient` already covers them
through `MockRestClient`; the NG proxy is API-incomplete on these surfaces and
we should drive coverage as those features ship rather than write tests that
have nothing to bind to.
+
+## Phase B — `org.apache.juneau.ng.http` coverage closeout
+
+A small set of parametric tests cover the bulk of the previously-uncovered
surface (named response classes, named header classes, body/part
implementations, and the static-factory facades) without committing to one test
class per type.
+
+- **`juneau-utest/.../ng/NgPackageScanner.java`** — shared utility that walks
the classpath (JARs or exploded `target/classes` directories) and enumerates
the concrete classes in a given package. Used by every parametric test below so
we don't hand-maintain class lists.
+- **`juneau-utest/.../ng/http/response/NgNamedResponses_Test.java`** —
parametric over every concrete subclass of `BasicHttpResponse` /
`BasicHttpException` in `org.apache.juneau.ng.http.response` (**57 tests**).
Reads each class's `STATUS_CODE` / `REASON_PHRASE` constants, exercises every
public constructor (including null cause / null message / null body paths),
verifies `getStatusCode`, `getStatusLine`, `getHeaders`, `toString`, and runs
`withBody(...)` / `withHeader(...)` mutator cha [...]
+- **`juneau-utest/.../ng/http/header/NgNamedHeaders_Test.java`** — parametric
over every concrete `HttpHeaderBean` subclass in
`org.apache.juneau.ng.http.header` (**53 tests**). Walks every public static
`of(...)`, `ofLazy*(...)` factory, supplying type-appropriate sample values
dispatched by the header's base type. Calls the typed accessors that match each
base class on **every instance built** — not just the last — so the
eager-value, wire-string, and lazy-supplier branches in `getValu [...]
+- **`juneau-utest/.../ng/http/header/PolymorphicHeaders_Test.java`** —
targeted coverage for the `IfRange` and `RetryAfter` polymorphic value branches
(**19 tests**). Exercises eager entity-tag, eager date, eager integer,
wire-string detection (numeric vs entity-tag-with-quote vs HTTP-date), lazy
suppliers returning each of the typed payloads, lazy suppliers returning null,
and null-input factory short-circuits.
+- **`juneau-utest/.../ng/http/response/HttpStatusLineBean_Test.java`** —
covers the `HttpStatusLineBean` factories, custom protocol version,
null-reason-phrase `toString` path, null-protocol-version rejection, and
`equals` / `hashCode` (**6 tests**).
+- **`juneau-utest/.../ng/http/entity/HttpBodies_Test.java`** — covers
`StringBody`, `ByteArrayBody`, `FileBody`, `StreamBody`, `HttpBodyBean`, and
`MultipartBody`: default vs explicit content type, repeatability flags,
defensive copy semantics, `writeTo(OutputStream)`, the multipart builder
pattern, and the part factories (**15 tests**).
+- **`juneau-utest/.../ng/http/part/HttpParts_Test.java`** — smoke tests for
`HttpPartBean` factories / `equals` / `hashCode` / `toString` and for
`PartList.getFirst` hit / miss plus the null-value skip path in
`PartList.writeTo` / `toString` (**~10 tests**).
+- **`juneau-utest/.../ng/http/HttpFactoryFacades_Test.java`** — parametric
over every public static method on the three façade classes (`HttpHeaders`,
`HttpBodies`, `HttpResponses`). Each is a thin delegation to an underlying
factory; reflectively invoking every overload with type-appropriate sample args
closes out ~300 otherwise-uncovered delegation instructions (**152 tests**).
+
+## Phase C / D — Verify + archive
+
+- `./scripts/coverage.py --run
juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/ng/http/` —
**95% / 80%**, ≥95% goal met.
+- Full `juneau-utest` suite: **50,491 tests, 0 failures, 0 errors**, ~33s
runtime.
+- This file replaces the live plan; `[TODO-11]` removed from `todo/TODO.md`.
+
+## Deferred (intentionally not closed by this pass)
+
+- **`org.apache.juneau.ng.http.remote.RrpcInterfaceMeta`** (~132 missed
instructions): the RRPC proxy hasn't been wired into the NG remote client yet,
so there is no useful end-to-end path to test. Pick this up when the NG remote
client grows `@Remote(rrpc=true)` support.
+- **NG remote-client gaps** (bean-shaped `@Query` / `@FormData`, `Reader` /
`InputStream` request bodies, multi-segment `@Path`, RRPC): write the
corresponding `NgRemoteInterfaceTransport_Test` scenarios as those features
ship.
+- **Hostile-server transport-edge cases** (truncated response body, malformed
status line, multi-value header carry-through, idle connection release under
abort): the parametric Jetty fixture from Phase A already exercises the happy
paths across all five transports. The remaining transport-side branches live
behind real wire faults and want a `com.sun.net.httpserver.HttpServer` harness
modelled on the existing `ApacheHc45Transport_Test` rather than the Juneau
pipeline used here. Track se [...]
diff --git a/todo/TODO-11-restclient-ng-coverage-closeout.md
b/todo/TODO-11-restclient-ng-coverage-closeout.md
deleted file mode 100644
index 43e74f13b4..0000000000
--- a/todo/TODO-11-restclient-ng-coverage-closeout.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# TODO-11: RestClient NG — Coverage Closeout + Cross-Transport
Remote-Interface Tests
-
-> **Status:** Implementation, docs, and release notes for the next-generation
REST client and HTTP stack (`org.apache.juneau.ng.*`) have shipped. The
original design plan is archived in
`todo/FINISHED-11a-restclient-ng-design-plan.md`. This file tracks the
**remaining** cleanup before `TODO-11` can be fully retired.
-
-## Remaining work
-
-### A. Cross-transport remote-interface tests (blocked on `[TODO-31]`)
-
-Existing `org.apache.juneau.http.remote` tests cover remote-interface behavior
end-to-end but only against the classic `MockRestClient` — they never go
through any real wire transport. The new NG transports each have their own
basic coverage against `com.sun.net.httpserver.HttpServer` with canned
responses, but nothing exercises a Juneau remote proxy through a real Juneau
REST pipeline on the server side.
-
-Once `[TODO-31]` lands, build:
-
-- `MicroserviceTestFixture` in `juneau-utest` — JUnit 5 extension that boots a
`JettyMicroservice` on port 0 from one or more `@Configuration` classes and
exposes `getRootUrl()`. Tears down in `afterAll`.
-- `NgRemoteInterfaceTransport_Test` in `juneau-utest` — parameterized over the
five NG transports (`apache-hc45`, `apache-hc5`, `java-http`, `okhttp`,
`jetty`) via `@ParameterizedTest` + `@MethodSource`. Server side is a single
`@Rest` resource provided as a `@Bean` from a test `@Configuration`.
-
-Focused scenario set per transport (each scenario runs 5x — once per
transport):
-
-- GET with `@Path` (single + multi-segment) and `@Query` (single + map + bean).
-- POST with `@Content` (string, bean, `Reader`, `InputStream`).
-- POST with `@FormData` (single, map, bean).
-- Header propagation (`@Header` on parameter, default headers on the client).
-- Response status: 200 with body, 204 no-content, 404 -> remote exception
mapping.
-- One end-to-end RRPC scenario through `@Remote(rrpc=true)` to confirm
bidirectional bean marshalling.
-
-5 transports x ~15 scenarios = ~75 executions. This is the primary lift for
transport-module coverage.
-
-### B. `org.apache.juneau.ng.http` coverage closeout (independent — can land
before TODO-31)
-
-The bulk of the uncovered surface is in named response classes (`Ok`,
`Created`, `NotFound`, ...) and RFC-named header classes (`Accept`,
`ContentType`, ...). Cover with two parametric tests:
-
--
`juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java`
— reflectively enumerate every public subclass of the response base in
`org.apache.juneau.ng.http.response`; for each, invoke every public constructor
/ factory, call all public getters, run `writeTo(OutputStream)`, verify status
code matches the RFC code.
--
`juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java`
— same parametric pattern over `org.apache.juneau.ng.http.header.*`. Walks
`of(...)` factories, value accessors, and `writeTo` / wire-format paths.
-- Fill targeted gaps in `HttpBody`, `HttpHeaders`, `HttpResource` discovered
by `./scripts/coverage.py --branches` (small one-shot tests).
-
-### C. Transport-module residual gaps (after A)
-
-After the parameterized suite from A has run, fill what remains via
`./scripts/coverage.py --branches`:
-
-- `close()` / connection-release hooks on each transport's response type.
-- Header carry-through for multi-value and quoted-value cases.
-- Streaming-body request paths (each transport's `InputStream` consumption
code).
-
-For hostile-server edge cases (truncated body, malformed status line), use
`com.sun.net.httpserver.HttpServer` following the existing pattern in
`ApacheHc45Transport_Test`.
-
-### D. Verify + archive
-
-- `./scripts/coverage.py --run` against
`juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/ng/http/`;
confirm >=95% instruction coverage.
-- `./scripts/coverage.py --branches` for every NG package; address remaining
gaps.
-- `./scripts/test.py` full suite; confirm no regressions and runtime stays in
the ~33s range.
-- Archive this file as `todo/FINISHED-11b-restclient-ng-coverage-closeout.md`
and remove `[TODO-11]` from `todo/TODO.md`.
-
-## Latest coverage measurement (May 2026)
-
-Measured via `./scripts/coverage.py` against `juneau-utest/target/jacoco.exec`.
-
-| Package / Module | Branches | Instructions |
-|---|---|---|
-| `org.apache.juneau.ng.http` (in `juneau-rest-common`) | 20% | **38%** |
-| `org.apache.juneau.ng.rest.client` (in `juneau-rest-client`) | 89% | 94% |
-| `org.apache.juneau.ng.rest.mock` (in `juneau-rest-mock`) | 98% | 96% |
-| Apache HC 4.5 transport (`juneau-ng-rest-client-apache-httpclient-45`) | 71%
| 81% |
-| Apache HC 5 transport (`juneau-ng-rest-client-apache-httpclient-50`) | 79% |
85% |
-| JDK `HttpClient` transport (`juneau-ng-rest-client-java-httpclient`) | 100%
| 78% |
-| OkHttp transport (`juneau-ng-rest-client-okhttp`) | 72% | 88% |
-| Jetty transport (`juneau-ng-rest-client-jetty`) | 75% | 77% |
-
-## Out of scope
-
-- Promoting the NG stack from beta to stable.
-- Deprecating or removing the classic `RestClient` / `juneau-rest-common`.
diff --git a/todo/TODO.md b/todo/TODO.md
index bab1163909..5fb2d9a748 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -17,8 +17,6 @@
- [TODO-10] Move `org.apache.juneau.http.annotation` from `juneau-marshall`
into `juneau-rest-common` (already done for the annotation classes — plan
tracks remaining follow-on cleanup). See
`todo/TODO-10-move-http-annotation-to-rest-common.md`.
-- [TODO-11] RestClient NG closeout: cross-transport remote-interface test
suite (blocked on [TODO-31]) plus NG http package coverage (independent).
Implementation has shipped (see
`todo/FINISHED-11a-restclient-ng-design-plan.md`). Active checklist in
`todo/TODO-11-restclient-ng-coverage-closeout.md`.
-
- [TODO-12] Schema validation mode for parsers and serializers: wire `@Schema`
validation into the bean property get/set lifecycle gated by a new
`validateSchema` flag on `MarshallingContext`. See
`todo/TODO-12-schema-validation.md`.
- [TODO-17] Audit 9.2.x changes (juneau-docs release notes 9.2.0 / 9.5.0 + git
history since 9.1.0) for breaking changes and populate the v9.5 Migration Guide
at juneau-docs/pages/topics/23.01.V9.5-migration-guide.md with Old→New rows for
each. Focus on removed APIs, renamed annotations/classes/methods, changed
default behaviors, and any annotation-attribute semantics changes.