junit.http: Extract AppServerBase to dedupe the EE8 AppServer overlay

The EE8 servlet bridge ships a hand-maintained
org.eclipse.jgit.junit.http.ee8 AppServer because the canonical
AppServer cannot be rewritten into its EE8 form automatically: the Jetty
security API (Constraint.Builder + ContextHandler juggling) differs
structurally from the Jetty EE8 one (ServletConstraint +
setSecurityHandler), which a jakarta->javax string rewrite cannot
bridge. The overlay was a 461-line copy; only ~40 lines diverge.

Split the common code into an abstract AppServerBase and reduce both
AppServers to the generation-specific part:

  AppServerBase            common server/context/login wiring plus the
                           protected abstract configureAuthentication()
                           method; transform-safe (only the
                           ServletContextHandler import renames ee10 ->
                           ee8).
  AppServer (canonical)    EE10: Constraint.Builder + setHandler.
  AppServer (ee8 overlay)  EE8: ServletConstraint + setSecurityHandler.
                           461 -> 103 lines.

Both toolchains pick AppServerBase up for free: the Bazel transform and
the Maven antrun copy both exclude only the file name AppServer.java, so
AppServerBase.java flows through the jakarta->javax / ee10->ee8 rewrite
while the hand-written EE8 AppServer.java stays the overlay. No BUILD or
pom changes. The class stays named AppServer in both worlds (separate
classpaths), so every "new AppServer(...)" site is untouched.

Moving the shared username/password constants into AppServerBase turns
the AppServer.username uses in the HTTP tests into an indirect static
access, which JGit's ecj settings escalate to a compile error (caught by
Maven, not by the Bazel javac build). Reference the declaring class
(AppServerBase.username) instead of re-declaring the constants on both
AppServers, which would reintroduce the duplication this change removes.

Alternative considered: renaming the EE8 overlay to EE8AppServer.
Rejected because a distinct name forces rewriting every
"new AppServer(...)" site in the transformed EE8 tests and couples the
transform to class names instead of imports.

Copyright is carried over unchanged from the split file.

Change-Id: I1d68bcc7d1d23668696ee91d8910f4940064bb37
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
index 3937627..2737a49 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
@@ -41,7 +41,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.http.AccessEvent;
-import org.eclipse.jgit.junit.http.AppServer;
+import org.eclipse.jgit.junit.http.AppServerBase;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -266,12 +266,12 @@ public void testListRemote_Dumb_Auth() throws Exception {
 		Repository dst = createBareRepository();
 		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
 			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
-					AppServer.username, AppServer.password));
+					AppServerBase.username, AppServerBase.password));
 			t.openFetch().close();
 		}
 		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
 			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
-					AppServer.username, ""));
+					AppServerBase.username, ""));
 			try {
 				t.openFetch();
 				fail("connection opened even info/refs needs auth basic and we provide wrong password");
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
index b0d17ad..46a48f7 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
@@ -68,6 +68,7 @@
 import org.eclipse.jgit.junit.TestRng;
 import org.eclipse.jgit.junit.http.AccessEvent;
 import org.eclipse.jgit.junit.http.AppServer;
+import org.eclipse.jgit.junit.http.AppServerBase;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -111,7 +112,7 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase {
 	private Repository remoteRepository;
 
 	private CredentialsProvider testCredentials = new UsernamePasswordCredentialsProvider(
-			AppServer.username, AppServer.password);
+			AppServerBase.username, AppServerBase.password);
 
 	private URIish remoteURI;
 
@@ -998,7 +999,7 @@ public void testInitialClone_WithPreAuthentication() throws Exception {
 				Transport t = Transport.open(dst, authURI)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			((TransportHttp) t).setPreemptiveBasicAuthentication(
-					AppServer.username, AppServer.password);
+					AppServerBase.username, AppServerBase.password);
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
 			assertTrue(dst.getObjectDatabase().has(A_txt));
 			assertEquals(B, dst.exactRef(master).getObjectId());
@@ -1018,7 +1019,7 @@ public void testInitialClone_WithPreAuthenticationCleared()
 				Transport t = Transport.open(dst, authURI)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			((TransportHttp) t).setPreemptiveBasicAuthentication(
-					AppServer.username, AppServer.password);
+					AppServerBase.username, AppServerBase.password);
 			((TransportHttp) t).setPreemptiveBasicAuthentication(null, null);
 			t.setCredentialsProvider(testCredentials);
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
@@ -1043,7 +1044,7 @@ public void testInitialClone_PreAuthenticationTooLate() throws Exception {
 				Transport t = Transport.open(dst, authURI)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			((TransportHttp) t).setPreemptiveBasicAuthentication(
-					AppServer.username, AppServer.password);
+					AppServerBase.username, AppServerBase.password);
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
 			assertTrue(dst.getObjectDatabase().has(A_txt));
 			assertEquals(B, dst.exactRef(master).getObjectId());
@@ -1053,7 +1054,7 @@ public void testInitialClone_PreAuthenticationTooLate() throws Exception {
 			assertFetchRequests(requests, 0);
 			assertThrows(IllegalStateException.class,
 					() -> ((TransportHttp) t).setPreemptiveBasicAuthentication(
-							AppServer.username, AppServer.password));
+							AppServerBase.username, AppServerBase.password));
 			assertThrows(IllegalStateException.class, () -> ((TransportHttp) t)
 					.setPreemptiveBasicAuthentication(null, null));
 		}
@@ -1066,7 +1067,7 @@ public void testInitialClone_WithWrongPreAuthenticationAndCredentialProvider()
 				Transport t = Transport.open(dst, authURI)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			((TransportHttp) t).setPreemptiveBasicAuthentication(
-					AppServer.username, AppServer.password + 'x');
+					AppServerBase.username, AppServerBase.password + 'x');
 			t.setCredentialsProvider(testCredentials);
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
 			assertTrue(dst.getObjectDatabase().has(A_txt));
@@ -1090,7 +1091,7 @@ public void testInitialClone_WithWrongPreAuthentication() throws Exception {
 				Transport t = Transport.open(dst, authURI)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			((TransportHttp) t).setPreemptiveBasicAuthentication(
-					AppServer.username, AppServer.password + 'x');
+					AppServerBase.username, AppServerBase.password + 'x');
 			TransportException e = assertThrows(TransportException.class,
 					() -> t.fetch(NullProgressMonitor.INSTANCE,
 							mirror(master)));
@@ -1108,8 +1109,8 @@ public void testInitialClone_WithWrongPreAuthentication() throws Exception {
 
 	@Test
 	public void testInitialClone_WithUserInfo() throws Exception {
-		URIish withUserInfo = authURI.setUser(AppServer.username)
-				.setPass(AppServer.password);
+		URIish withUserInfo = authURI.setUser(AppServerBase.username)
+				.setPass(AppServerBase.password);
 		try (Repository dst = createBareRepository();
 				Transport t = Transport.open(dst, withUserInfo)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
@@ -1127,13 +1128,13 @@ public void testInitialClone_WithUserInfo() throws Exception {
 
 	@Test
 	public void testInitialClone_PreAuthOverridesUserInfo() throws Exception {
-		URIish withUserInfo = authURI.setUser(AppServer.username)
-				.setPass(AppServer.password + 'x');
+		URIish withUserInfo = authURI.setUser(AppServerBase.username)
+				.setPass(AppServerBase.password + 'x');
 		try (Repository dst = createBareRepository();
 				Transport t = Transport.open(dst, withUserInfo)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			((TransportHttp) t).setPreemptiveBasicAuthentication(
-					AppServer.username, AppServer.password);
+					AppServerBase.username, AppServerBase.password);
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
 			assertTrue(dst.getObjectDatabase().has(A_txt));
 			assertEquals(B, dst.exactRef(master).getObjectId());
@@ -1174,7 +1175,7 @@ public void testInitialClone_WithAuthenticationWrongCredentials()
 				Transport t = Transport.open(dst, authURI)) {
 			assertFalse(dst.getObjectDatabase().has(A_txt));
 			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
-					AppServer.username, "wrongpassword"));
+					AppServerBase.username, "wrongpassword"));
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
 			fail("Should not have succeeded -- wrong password");
 		} catch (TransportException e) {
diff --git a/org.eclipse.jgit.junit.http.ee8/src/org/eclipse/jgit/junit/http/AppServer.java b/org.eclipse.jgit.junit.http.ee8/src/org/eclipse/jgit/junit/http/AppServer.java
index 1e4c796..3627d92 100644
--- a/org.eclipse.jgit.junit.http.ee8/src/org/eclipse/jgit/junit/http/AppServer.java
+++ b/org.eclipse.jgit.junit.http.ee8/src/org/eclipse/jgit/junit/http/AppServer.java
@@ -10,95 +10,34 @@
 
 package org.eclipse.jgit.junit.http;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.UnknownHostException;
-import java.nio.file.Files;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jetty.ee8.nested.ServletConstraint;
-import org.eclipse.jetty.ee8.security.Authenticator;
 import org.eclipse.jetty.ee8.security.ConstraintMapping;
 import org.eclipse.jetty.ee8.security.ConstraintSecurityHandler;
 import org.eclipse.jetty.ee8.security.authentication.BasicAuthenticator;
 import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
 import org.eclipse.jetty.security.AbstractLoginService;
-import org.eclipse.jetty.security.RolePrincipal;
-import org.eclipse.jetty.security.UserPrincipal;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.HttpConfiguration;
-import org.eclipse.jetty.server.HttpConnectionFactory;
-import org.eclipse.jetty.server.SecureRequestCustomizer;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.server.SslConnectionFactory;
-import org.eclipse.jetty.server.handler.ContextHandlerCollection;
-import org.eclipse.jetty.util.security.Password;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.eclipse.jgit.transport.URIish;
 
 /**
- * Tiny web application server for unit testing.
+ * Tiny web application server for unit testing, wired for the EE8
+ * (javax.servlet / Jetty EE8) generation.
  * <p>
- * Tests should start the server in their {@code setUp()} method and stop the
- * server in their {@code tearDown()} method. Only while started the server's
- * URL and/or port number can be obtained.
+ * Only the Jetty security wiring is generation specific; everything else is
+ * inherited from the generated {@link AppServerBase}. This is the hand
+ * maintained EE8 overlay that the canonical {@code AppServer} cannot be
+ * rewritten into automatically, because the Jetty EE8 security API
+ * ({@code ServletConstraint} + {@code setSecurityHandler}) differs structurally
+ * from the EE10 one.
  */
-public class AppServer {
-	/** Realm name for the secure access areas. */
-	public static final String realm = "Secure Area";
-
-	/** Username for secured access areas. */
-	public static final String username = "agitter";
-
-	/** Password for {@link #username} in secured access areas. */
-	public static final String password = "letmein";
-
-	/** SSL keystore password; must have at least 6 characters. */
-	private static final String keyPassword = "mykeys";
-
-	/** Role for authentication. */
-	private static final String authRole = "can-access";
-
-	static {
-		// Install a logger that throws warning messages.
-		//
-		final String prop = "org.eclipse.jetty.util.log.class";
-		System.setProperty(prop, RecordingLogger.class.getName());
-	}
-
-	private final Server server;
-
-	private final HttpConfiguration config;
-
-	private final ServerConnector connector;
-
-	private final HttpConfiguration secureConfig;
-
-	private final ServerConnector secureConnector;
-
-	private final ContextHandlerCollection contexts;
-
-	private final TestRequestLog log;
-
-	private List<File> filesToDelete = new ArrayList<>();
-
+public class AppServer extends AppServerBase {
 	/**
 	 * Constructor for <code>AppServer</code>.
 	 */
 	public AppServer() {
-		this(0, -1);
+		super();
 	}
 
 	/**
@@ -110,7 +49,7 @@ public AppServer() {
 	 * @since 4.2
 	 */
 	public AppServer(int port) {
-		this(port, -1);
+		super(port);
 	}
 
 	/**
@@ -124,185 +63,7 @@ public AppServer(int port) {
 	 * @since 4.9
 	 */
 	public AppServer(int port, int sslPort) {
-		server = new Server();
-
-		config = new HttpConfiguration();
-		config.setSecureScheme("https");
-		config.setSecurePort(0);
-		config.setOutputBufferSize(32768);
-
-		connector = new ServerConnector(server,
-				new HttpConnectionFactory(config));
-		connector.setPort(port);
-		String ip;
-		String hostName;
-		try {
-			final InetAddress me = InetAddress.getByName("localhost");
-			ip = me.getHostAddress();
-			connector.setHost(ip);
-			hostName = InetAddress.getLocalHost().getCanonicalHostName();
-		} catch (UnknownHostException e) {
-			throw new RuntimeException("Cannot find localhost", e);
-		}
-
-		if (sslPort >= 0) {
-			SslContextFactory.Server sslContextFactory = createTestSslContextFactory(
-					hostName, ip);
-			secureConfig = new HttpConfiguration(config);
-			secureConfig.addCustomizer(new SecureRequestCustomizer());
-			HttpConnectionFactory http11 = new HttpConnectionFactory(
-					secureConfig);
-			SslConnectionFactory tls = new SslConnectionFactory(
-					sslContextFactory, http11.getProtocol());
-			secureConnector = new ServerConnector(server, tls, http11);
-			secureConnector.setPort(sslPort);
-			secureConnector.setHost(ip);
-		} else {
-			secureConfig = null;
-			secureConnector = null;
-		}
-
-		contexts = new ContextHandlerCollection();
-
-		log = new TestRequestLog();
-		log.setHandler(contexts);
-
-		if (secureConnector == null) {
-			server.setConnectors(new Connector[] { connector });
-		} else {
-			server.setConnectors(
-					new Connector[] { connector, secureConnector });
-		}
-		server.setHandler(log);
-	}
-
-	private SslContextFactory.Server createTestSslContextFactory(
-			String hostName, String ip) {
-		SslContextFactory.Server factory = new SslContextFactory.Server();
-
-		String dName = "CN=localhost,OU=JGit,O=Eclipse,ST=Ontario,L=Toronto,C=CA";
-
-		try {
-			File tmpDir = Files.createTempDirectory("jks").toFile();
-			tmpDir.deleteOnExit();
-			makePrivate(tmpDir);
-			File keyStore = new File(tmpDir, "keystore.jks");
-			File keytool = new File(
-					new File(new File(System.getProperty("java.home")), "bin"),
-					"keytool");
-			Runtime.getRuntime().exec(
-					new String[] {
-							keytool.getAbsolutePath(), //
-							"-keystore", keyStore.getAbsolutePath(), //
-							"-storepass", keyPassword,
-							"-alias", hostName, //
-							"-ext", "bc=ca:true", //
-							"-ext",
-							String.format(
-									"san=ip:%s,ip:127.0.0.1,ip:[::1],DNS:%s",
-									ip, hostName), //
-							"-genkeypair", //
-							"-keyalg", "RSA", //
-							"-keypass", keyPassword, //
-							"-dname", dName, //
-							"-validity", "2" //
-					}).waitFor();
-			keyStore.deleteOnExit();
-			makePrivate(keyStore);
-			filesToDelete.add(keyStore);
-			filesToDelete.add(tmpDir);
-			factory.setKeyStorePath(keyStore.getAbsolutePath());
-			factory.setKeyStorePassword(keyPassword);
-			factory.setKeyManagerPassword(keyPassword);
-			factory.setTrustStorePath(keyStore.getAbsolutePath());
-			factory.setTrustStorePassword(keyPassword);
-		} catch (InterruptedException | IOException e) {
-			throw new RuntimeException("Cannot create ssl key/certificate", e);
-		}
-		return factory;
-	}
-
-	private void makePrivate(File file) {
-		file.setReadable(false);
-		file.setWritable(false);
-		file.setExecutable(false);
-		file.setReadable(true, true);
-		file.setWritable(true, true);
-		if (file.isDirectory()) {
-			file.setExecutable(true, true);
-		}
-	}
-
-	/**
-	 * Create a new servlet context within the server.
-	 * <p>
-	 * This method should be invoked before the server is started, once for each
-	 * context the caller wants to register.
-	 *
-	 * @param path
-	 *            path of the context; use "/" for the root context if binding
-	 *            to the root is desired.
-	 * @return the context to add servlets into.
-	 * @since 7.0
-	 */
-	public ServletContextHandler addContext(String path) {
-		assertNotYetSetUp();
-		if ("".equals(path))
-			path = "/";
-
-		ServletContextHandler ctx = new ServletContextHandler();
-		ctx.setContextPath(path);
-		contexts.addHandler(ctx);
-
-		return ctx;
-	}
-
-	/**
-	 * Configure basic authentication.
-	 *
-	 * @param ctx
-	 *            servlet context handler
-	 * @param methods
-	 *            the methods
-	 * @return servlet context handler
-	 * @since 7.0
-	 */
-	public ServletContextHandler authBasic(ServletContextHandler ctx,
-			String... methods) {
-		assertNotYetSetUp();
-		auth(ctx, new BasicAuthenticator(), methods);
-		return ctx;
-	}
-
-	static class TestMappedLoginService extends AbstractLoginService {
-		private RolePrincipal role;
-
-		protected final Map<String, UserPrincipal> users = new ConcurrentHashMap<>();
-
-		TestMappedLoginService(String role) {
-			this.role = new RolePrincipal(role);
-		}
-
-		@Override
-		protected void doStart() throws Exception {
-			UserPrincipal p = new UserPrincipal(username,
-					new Password(password));
-			users.put(username, p);
-			super.doStart();
-		}
-
-		@Override
-		protected UserPrincipal loadUserInfo(String user) {
-			return users.get(user);
-		}
-
-		@Override
-		protected List<RolePrincipal> loadRoleInfo(UserPrincipal user) {
-			if (users.get(user.getName()) == null) {
-				return null;
-			}
-			return Collections.singletonList(role);
-		}
+		super(port, sslPort);
 	}
 
 	private ConstraintMapping createConstraintMapping() {
@@ -314,8 +75,9 @@ private ConstraintMapping createConstraintMapping() {
 		return cm;
 	}
 
-	private void auth(ServletContextHandler ctx, Authenticator authType,
-			String... methods) {
+	@Override
+	protected void configureAuthentication(ServletContextHandler ctx,
+			String[] methods) {
 		AbstractLoginService users = new TestMappedLoginService(authRole);
 		List<ConstraintMapping> mappings = new ArrayList<>();
 		if (methods == null || methods.length == 0) {
@@ -333,129 +95,9 @@ private void auth(ServletContextHandler ctx, Authenticator authType,
 
 		ConstraintSecurityHandler sec = new ConstraintSecurityHandler();
 		sec.setRealmName(realm);
-		sec.setAuthenticator(authType);
+		sec.setAuthenticator(new BasicAuthenticator());
 		sec.setLoginService(users);
 		sec.setConstraintMappings(mappings.toArray(new ConstraintMapping[0]));
 		ctx.setSecurityHandler(sec);
 	}
-
-	/**
-	 * Start the server on a random local port.
-	 *
-	 * @throws Exception
-	 *             the server cannot be started, testing is not possible.
-	 */
-	public void setUp() throws Exception {
-		RecordingLogger.clear();
-		log.clear();
-		server.start();
-		config.setSecurePort(getSecurePort());
-		if (secureConfig != null) {
-			secureConfig.setSecurePort(getSecurePort());
-		}
-	}
-
-	/**
-	 * Shutdown the server.
-	 *
-	 * @throws Exception
-	 *             the server refuses to halt, or wasn't running.
-	 */
-	public void tearDown() throws Exception {
-		RecordingLogger.clear();
-		log.clear();
-		server.stop();
-		for (File f : filesToDelete) {
-			f.delete();
-		}
-		filesToDelete.clear();
-	}
-
-	/**
-	 * Get the URI to reference this server.
-	 * <p>
-	 * The returned URI includes the proper host name and port number, but does
-	 * not contain a path.
-	 *
-	 * @return URI to reference this server's root context.
-	 */
-	public URI getURI() {
-		assertAlreadySetUp();
-		String host = connector.getHost();
-		if (host.contains(":") && !host.startsWith("["))
-			host = "[" + host + "]";
-		final String uri = "http://" + host + ":" + getPort();
-		try {
-			return new URI(uri);
-		} catch (URISyntaxException e) {
-			throw new RuntimeException("Unexpected URI error on " + uri, e);
-		}
-	}
-
-	/**
-	 * Get port.
-	 *
-	 * @return the local port number the server is listening on.
-	 */
-	public int getPort() {
-		assertAlreadySetUp();
-		return connector.getLocalPort();
-	}
-
-	/**
-	 * Get secure port.
-	 *
-	 * @return the HTTPS port or -1 if not configured.
-	 */
-	public int getSecurePort() {
-		assertAlreadySetUp();
-		return secureConnector != null ? secureConnector.getLocalPort() : -1;
-	}
-
-	/**
-	 * Get requests.
-	 *
-	 * @return all requests since the server was started.
-	 */
-	public List<AccessEvent> getRequests() {
-		return new ArrayList<>(log.getEvents());
-	}
-
-	/**
-	 * Get requests.
-	 *
-	 * @param base
-	 *            base URI used to access the server.
-	 * @param path
-	 *            the path to locate requests for, relative to {@code base}.
-	 * @return all requests which match the given path.
-	 */
-	public List<AccessEvent> getRequests(URIish base, String path) {
-		return getRequests(HttpTestCase.join(base, path));
-	}
-
-	/**
-	 * Get requests.
-	 *
-	 * @param path
-	 *            the path to locate requests for.
-	 * @return all requests which match the given path.
-	 */
-	public List<AccessEvent> getRequests(String path) {
-		ArrayList<AccessEvent> r = new ArrayList<>();
-		for (AccessEvent event : log.getEvents()) {
-			if (event.getPath().equals(path)) {
-				r.add(event);
-			}
-		}
-		return r;
-	}
-
-	private void assertNotYetSetUp() {
-		assertFalse("server is not running", server.isRunning());
-	}
-
-	private void assertAlreadySetUp() {
-		assertTrue("server is running", server.isRunning());
-	}
 }
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
index 76e437b..3a4bce7 100644
--- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
+++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
@@ -10,95 +10,30 @@
 
 package org.eclipse.jgit.junit.http;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.UnknownHostException;
-import java.nio.file.Files;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
 import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
 import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
 import org.eclipse.jetty.security.AbstractLoginService;
-import org.eclipse.jetty.security.Authenticator;
 import org.eclipse.jetty.security.Constraint;
-import org.eclipse.jetty.security.RolePrincipal;
-import org.eclipse.jetty.security.UserPrincipal;
 import org.eclipse.jetty.security.authentication.BasicAuthenticator;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.HttpConfiguration;
-import org.eclipse.jetty.server.HttpConnectionFactory;
-import org.eclipse.jetty.server.SecureRequestCustomizer;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.server.SslConnectionFactory;
-import org.eclipse.jetty.server.handler.ContextHandlerCollection;
-import org.eclipse.jetty.util.security.Password;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.eclipse.jgit.transport.URIish;
 
 /**
- * Tiny web application server for unit testing.
+ * Tiny web application server for unit testing, wired for the Jakarta servlet
+ * (Jetty EE10) generation.
  * <p>
- * Tests should start the server in their {@code setUp()} method and stop the
- * server in their {@code tearDown()} method. Only while started the server's
- * URL and/or port number can be obtained.
+ * Only the Jetty security wiring is generation specific; everything else lives
+ * in {@link AppServerBase}.
  */
-public class AppServer {
-	/** Realm name for the secure access areas. */
-	public static final String realm = "Secure Area";
-
-	/** Username for secured access areas. */
-	public static final String username = "agitter";
-
-	/** Password for {@link #username} in secured access areas. */
-	public static final String password = "letmein";
-
-	/** SSL keystore password; must have at least 6 characters. */
-	private static final String keyPassword = "mykeys";
-
-	/** Role for authentication. */
-	private static final String authRole = "can-access";
-
-	static {
-		// Install a logger that throws warning messages.
-		//
-		final String prop = "org.eclipse.jetty.util.log.class";
-		System.setProperty(prop, RecordingLogger.class.getName());
-	}
-
-	private final Server server;
-
-	private final HttpConfiguration config;
-
-	private final ServerConnector connector;
-
-	private final HttpConfiguration secureConfig;
-
-	private final ServerConnector secureConnector;
-
-	private final ContextHandlerCollection contexts;
-
-	private final TestRequestLog log;
-
-	private List<File> filesToDelete = new ArrayList<>();
-
+public class AppServer extends AppServerBase {
 	/**
 	 * Constructor for <code>AppServer</code>.
 	 */
 	public AppServer() {
-		this(0, -1);
+		super();
 	}
 
 	/**
@@ -110,7 +45,7 @@ public AppServer() {
 	 * @since 4.2
 	 */
 	public AppServer(int port) {
-		this(port, -1);
+		super(port);
 	}
 
 	/**
@@ -124,185 +59,7 @@ public AppServer(int port) {
 	 * @since 4.9
 	 */
 	public AppServer(int port, int sslPort) {
-		server = new Server();
-
-		config = new HttpConfiguration();
-		config.setSecureScheme("https");
-		config.setSecurePort(0);
-		config.setOutputBufferSize(32768);
-
-		connector = new ServerConnector(server,
-				new HttpConnectionFactory(config));
-		connector.setPort(port);
-		String ip;
-		String hostName;
-		try {
-			final InetAddress me = InetAddress.getByName("localhost");
-			ip = me.getHostAddress();
-			connector.setHost(ip);
-			hostName = InetAddress.getLocalHost().getCanonicalHostName();
-		} catch (UnknownHostException e) {
-			throw new RuntimeException("Cannot find localhost", e);
-		}
-
-		if (sslPort >= 0) {
-			SslContextFactory.Server sslContextFactory = createTestSslContextFactory(
-					hostName, ip);
-			secureConfig = new HttpConfiguration(config);
-			secureConfig.addCustomizer(new SecureRequestCustomizer());
-			HttpConnectionFactory http11 = new HttpConnectionFactory(
-					secureConfig);
-			SslConnectionFactory tls = new SslConnectionFactory(
-					sslContextFactory, http11.getProtocol());
-			secureConnector = new ServerConnector(server, tls, http11);
-			secureConnector.setPort(sslPort);
-			secureConnector.setHost(ip);
-		} else {
-			secureConfig = null;
-			secureConnector = null;
-		}
-
-		contexts = new ContextHandlerCollection();
-
-		log = new TestRequestLog();
-		log.setHandler(contexts);
-
-		if (secureConnector == null) {
-			server.setConnectors(new Connector[] { connector });
-		} else {
-			server.setConnectors(
-					new Connector[] { connector, secureConnector });
-		}
-		server.setHandler(log);
-	}
-
-	private SslContextFactory.Server createTestSslContextFactory(
-			String hostName, String ip) {
-		SslContextFactory.Server factory = new SslContextFactory.Server();
-
-		String dName = "CN=localhost,OU=JGit,O=Eclipse,ST=Ontario,L=Toronto,C=CA";
-
-		try {
-			File tmpDir = Files.createTempDirectory("jks").toFile();
-			tmpDir.deleteOnExit();
-			makePrivate(tmpDir);
-			File keyStore = new File(tmpDir, "keystore.jks");
-			File keytool = new File(
-					new File(new File(System.getProperty("java.home")), "bin"),
-					"keytool");
-			Runtime.getRuntime().exec(
-					new String[] {
-							keytool.getAbsolutePath(), //
-							"-keystore", keyStore.getAbsolutePath(), //
-							"-storepass", keyPassword,
-							"-alias", hostName, //
-							"-ext", "bc=ca:true", //
-							"-ext",
-							String.format(
-									"san=ip:%s,ip:127.0.0.1,ip:[::1],DNS:%s",
-									ip, hostName), //
-							"-genkeypair", //
-							"-keyalg", "RSA", //
-							"-keypass", keyPassword, //
-							"-dname", dName, //
-							"-validity", "2" //
-					}).waitFor();
-			keyStore.deleteOnExit();
-			makePrivate(keyStore);
-			filesToDelete.add(keyStore);
-			filesToDelete.add(tmpDir);
-			factory.setKeyStorePath(keyStore.getAbsolutePath());
-			factory.setKeyStorePassword(keyPassword);
-			factory.setKeyManagerPassword(keyPassword);
-			factory.setTrustStorePath(keyStore.getAbsolutePath());
-			factory.setTrustStorePassword(keyPassword);
-		} catch (InterruptedException | IOException e) {
-			throw new RuntimeException("Cannot create ssl key/certificate", e);
-		}
-		return factory;
-	}
-
-	private void makePrivate(File file) {
-		file.setReadable(false);
-		file.setWritable(false);
-		file.setExecutable(false);
-		file.setReadable(true, true);
-		file.setWritable(true, true);
-		if (file.isDirectory()) {
-			file.setExecutable(true, true);
-		}
-	}
-
-	/**
-	 * Create a new servlet context within the server.
-	 * <p>
-	 * This method should be invoked before the server is started, once for each
-	 * context the caller wants to register.
-	 *
-	 * @param path
-	 *            path of the context; use "/" for the root context if binding
-	 *            to the root is desired.
-	 * @return the context to add servlets into.
-	 * @since 7.0
-	 */
-	public ServletContextHandler addContext(String path) {
-		assertNotYetSetUp();
-		if ("".equals(path))
-			path = "/";
-
-		ServletContextHandler ctx = new ServletContextHandler();
-		ctx.setContextPath(path);
-		contexts.addHandler(ctx);
-
-		return ctx;
-	}
-
-	/**
-	 * Configure basic authentication.
-	 *
-	 * @param ctx
-	 *            servlet context handler
-	 * @param methods
-	 *            the methods
-	 * @return servlet context handler
-	 * @since 7.0
-	 */
-	public ServletContextHandler authBasic(ServletContextHandler ctx,
-			String... methods) {
-		assertNotYetSetUp();
-		auth(ctx, new BasicAuthenticator(), methods);
-		return ctx;
-	}
-
-	static class TestMappedLoginService extends AbstractLoginService {
-		private RolePrincipal role;
-
-		protected final Map<String, UserPrincipal> users = new ConcurrentHashMap<>();
-
-		TestMappedLoginService(String role) {
-			this.role = new RolePrincipal(role);
-		}
-
-		@Override
-		protected void doStart() throws Exception {
-			UserPrincipal p = new UserPrincipal(username,
-					new Password(password));
-			users.put(username, p);
-			super.doStart();
-		}
-
-		@Override
-		protected UserPrincipal loadUserInfo(String user) {
-			return users.get(user);
-		}
-
-		@Override
-		protected List<RolePrincipal> loadRoleInfo(UserPrincipal user) {
-			if (users.get(user.getName()) == null) {
-				return null;
-			}
-			return Collections.singletonList(role);
-		}
+		super(port, sslPort);
 	}
 
 	private ConstraintMapping createConstraintMapping() {
@@ -315,8 +72,9 @@ private ConstraintMapping createConstraintMapping() {
 		return cm;
 	}
 
-	private void auth(ServletContextHandler ctx, Authenticator authType,
-			String... methods) {
+	@Override
+	protected void configureAuthentication(ServletContextHandler ctx,
+			String[] methods) {
 		AbstractLoginService users = new TestMappedLoginService(authRole);
 		List<ConstraintMapping> mappings = new ArrayList<>();
 		if (methods == null || methods.length == 0) {
@@ -331,133 +89,12 @@ private void auth(ServletContextHandler ctx, Authenticator authType,
 
 		ConstraintSecurityHandler sec = new ConstraintSecurityHandler();
 		sec.setRealmName(realm);
-		sec.setAuthenticator(authType);
+		sec.setAuthenticator(new BasicAuthenticator());
 		sec.setLoginService(users);
-		sec.setConstraintMappings(
-				mappings.toArray(new ConstraintMapping[0]));
+		sec.setConstraintMappings(mappings.toArray(new ConstraintMapping[0]));
 		sec.setHandler(ctx);
 
 		contexts.removeHandler(ctx);
 		contexts.addHandler(sec);
 	}
-
-	/**
-	 * Start the server on a random local port.
-	 *
-	 * @throws Exception
-	 *             the server cannot be started, testing is not possible.
-	 */
-	public void setUp() throws Exception {
-		RecordingLogger.clear();
-		log.clear();
-		server.start();
-		config.setSecurePort(getSecurePort());
-		if (secureConfig != null) {
-			secureConfig.setSecurePort(getSecurePort());
-		}
-	}
-
-	/**
-	 * Shutdown the server.
-	 *
-	 * @throws Exception
-	 *             the server refuses to halt, or wasn't running.
-	 */
-	public void tearDown() throws Exception {
-		RecordingLogger.clear();
-		log.clear();
-		server.stop();
-		for (File f : filesToDelete) {
-			f.delete();
-		}
-		filesToDelete.clear();
-	}
-
-	/**
-	 * Get the URI to reference this server.
-	 * <p>
-	 * The returned URI includes the proper host name and port number, but does
-	 * not contain a path.
-	 *
-	 * @return URI to reference this server's root context.
-	 */
-	public URI getURI() {
-		assertAlreadySetUp();
-		String host = connector.getHost();
-		if (host.contains(":") && !host.startsWith("["))
-			host = "[" + host + "]";
-		final String uri = "http://" + host + ":" + getPort();
-		try {
-			return new URI(uri);
-		} catch (URISyntaxException e) {
-			throw new RuntimeException("Unexpected URI error on " + uri, e);
-		}
-	}
-
-	/**
-	 * Get port.
-	 *
-	 * @return the local port number the server is listening on.
-	 */
-	public int getPort() {
-		assertAlreadySetUp();
-		return connector.getLocalPort();
-	}
-
-	/**
-	 * Get secure port.
-	 *
-	 * @return the HTTPS port or -1 if not configured.
-	 */
-	public int getSecurePort() {
-		assertAlreadySetUp();
-		return secureConnector != null ? secureConnector.getLocalPort() : -1;
-	}
-
-	/**
-	 * Get requests.
-	 *
-	 * @return all requests since the server was started.
-	 */
-	public List<AccessEvent> getRequests() {
-		return new ArrayList<>(log.getEvents());
-	}
-
-	/**
-	 * Get requests.
-	 *
-	 * @param base
-	 *            base URI used to access the server.
-	 * @param path
-	 *            the path to locate requests for, relative to {@code base}.
-	 * @return all requests which match the given path.
-	 */
-	public List<AccessEvent> getRequests(URIish base, String path) {
-		return getRequests(HttpTestCase.join(base, path));
-	}
-
-	/**
-	 * Get requests.
-	 *
-	 * @param path
-	 *            the path to locate requests for.
-	 * @return all requests which match the given path.
-	 */
-	public List<AccessEvent> getRequests(String path) {
-		ArrayList<AccessEvent> r = new ArrayList<>();
-		for (AccessEvent event : log.getEvents()) {
-			if (event.getPath().equals(path)) {
-				r.add(event);
-			}
-		}
-		return r;
-	}
-
-	private void assertNotYetSetUp() {
-		assertFalse("server is not running", server.isRunning());
-	}
-
-	private void assertAlreadySetUp() {
-		assertTrue("server is running", server.isRunning());
-	}
 }
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServerBase.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServerBase.java
new file mode 100644
index 0000000..bc2baad
--- /dev/null
+++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServerBase.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2010, 2017 Google Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.junit.http;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.security.AbstractLoginService;
+import org.eclipse.jetty.security.RolePrincipal;
+import org.eclipse.jetty.security.UserPrincipal;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.util.security.Password;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jgit.transport.URIish;
+
+/**
+ * Tiny web application server for unit testing.
+ * <p>
+ * Tests should start the server in their {@code setUp()} method and stop the
+ * server in their {@code tearDown()} method. Only while started the server's
+ * URL and/or port number can be obtained.
+ * <p>
+ * The Jetty security API for declaring authentication constraints differs
+ * between EE generations. That part is therefore isolated in the abstract
+ * {@link #configureAuthentication(ServletContextHandler, String[])} seam, which
+ * the concrete {@code AppServer} provides for its servlet generation.
+ */
+public abstract class AppServerBase {
+	/** Realm name for the secure access areas. */
+	public static final String realm = "Secure Area";
+
+	/** Username for secured access areas. */
+	public static final String username = "agitter";
+
+	/** Password for {@link #username} in secured access areas. */
+	public static final String password = "letmein";
+
+	/** SSL keystore password; must have at least 6 characters. */
+	private static final String keyPassword = "mykeys";
+
+	/** Role for authentication. */
+	protected static final String authRole = "can-access";
+
+	static {
+		// Install a logger that throws warning messages.
+		//
+		final String prop = "org.eclipse.jetty.util.log.class";
+		System.setProperty(prop, RecordingLogger.class.getName());
+	}
+
+	private final Server server;
+
+	private final HttpConfiguration config;
+
+	private final ServerConnector connector;
+
+	private final HttpConfiguration secureConfig;
+
+	private final ServerConnector secureConnector;
+
+	/** Collection the servlet contexts are registered in. */
+	protected final ContextHandlerCollection contexts;
+
+	private final TestRequestLog log;
+
+	private List<File> filesToDelete = new ArrayList<>();
+
+	/**
+	 * Constructor for <code>AppServer</code>.
+	 */
+	protected AppServerBase() {
+		this(0, -1);
+	}
+
+	/**
+	 * Constructor for <code>AppServer</code>.
+	 *
+	 * @param port
+	 *            the http port number; may be zero to allocate a port
+	 *            dynamically
+	 */
+	protected AppServerBase(int port) {
+		this(port, -1);
+	}
+
+	/**
+	 * Constructor for <code>AppServer</code>.
+	 *
+	 * @param port
+	 *            for http, may be zero to allocate a port dynamically
+	 * @param sslPort
+	 *            for https,may be zero to allocate a port dynamically. If
+	 *            negative, the server will be set up without https support.
+	 */
+	protected AppServerBase(int port, int sslPort) {
+		server = new Server();
+
+		config = new HttpConfiguration();
+		config.setSecureScheme("https");
+		config.setSecurePort(0);
+		config.setOutputBufferSize(32768);
+
+		connector = new ServerConnector(server,
+				new HttpConnectionFactory(config));
+		connector.setPort(port);
+		String ip;
+		String hostName;
+		try {
+			final InetAddress me = InetAddress.getByName("localhost");
+			ip = me.getHostAddress();
+			connector.setHost(ip);
+			hostName = InetAddress.getLocalHost().getCanonicalHostName();
+		} catch (UnknownHostException e) {
+			throw new RuntimeException("Cannot find localhost", e);
+		}
+
+		if (sslPort >= 0) {
+			SslContextFactory.Server sslContextFactory = createTestSslContextFactory(
+					hostName, ip);
+			secureConfig = new HttpConfiguration(config);
+			secureConfig.addCustomizer(new SecureRequestCustomizer());
+			HttpConnectionFactory http11 = new HttpConnectionFactory(
+					secureConfig);
+			SslConnectionFactory tls = new SslConnectionFactory(
+					sslContextFactory, http11.getProtocol());
+			secureConnector = new ServerConnector(server, tls, http11);
+			secureConnector.setPort(sslPort);
+			secureConnector.setHost(ip);
+		} else {
+			secureConfig = null;
+			secureConnector = null;
+		}
+
+		contexts = new ContextHandlerCollection();
+
+		log = new TestRequestLog();
+		log.setHandler(contexts);
+
+		if (secureConnector == null) {
+			server.setConnectors(new Connector[] { connector });
+		} else {
+			server.setConnectors(
+					new Connector[] { connector, secureConnector });
+		}
+		server.setHandler(log);
+	}
+
+	private SslContextFactory.Server createTestSslContextFactory(
+			String hostName, String ip) {
+		SslContextFactory.Server factory = new SslContextFactory.Server();
+
+		String dName = "CN=localhost,OU=JGit,O=Eclipse,ST=Ontario,L=Toronto,C=CA";
+
+		try {
+			File tmpDir = Files.createTempDirectory("jks").toFile();
+			tmpDir.deleteOnExit();
+			makePrivate(tmpDir);
+			File keyStore = new File(tmpDir, "keystore.jks");
+			File keytool = new File(
+					new File(new File(System.getProperty("java.home")), "bin"),
+					"keytool");
+			Runtime.getRuntime().exec(
+					new String[] {
+							keytool.getAbsolutePath(), //
+							"-keystore", keyStore.getAbsolutePath(), //
+							"-storepass", keyPassword,
+							"-alias", hostName, //
+							"-ext", "bc=ca:true", //
+							"-ext",
+							String.format(
+									"san=ip:%s,ip:127.0.0.1,ip:[::1],DNS:%s",
+									ip, hostName), //
+							"-genkeypair", //
+							"-keyalg", "RSA", //
+							"-keypass", keyPassword, //
+							"-dname", dName, //
+							"-validity", "2" //
+					}).waitFor();
+			keyStore.deleteOnExit();
+			makePrivate(keyStore);
+			filesToDelete.add(keyStore);
+			filesToDelete.add(tmpDir);
+			factory.setKeyStorePath(keyStore.getAbsolutePath());
+			factory.setKeyStorePassword(keyPassword);
+			factory.setKeyManagerPassword(keyPassword);
+			factory.setTrustStorePath(keyStore.getAbsolutePath());
+			factory.setTrustStorePassword(keyPassword);
+		} catch (InterruptedException | IOException e) {
+			throw new RuntimeException("Cannot create ssl key/certificate", e);
+		}
+		return factory;
+	}
+
+	private void makePrivate(File file) {
+		file.setReadable(false);
+		file.setWritable(false);
+		file.setExecutable(false);
+		file.setReadable(true, true);
+		file.setWritable(true, true);
+		if (file.isDirectory()) {
+			file.setExecutable(true, true);
+		}
+	}
+
+	/**
+	 * Create a new servlet context within the server.
+	 * <p>
+	 * This method should be invoked before the server is started, once for each
+	 * context the caller wants to register.
+	 *
+	 * @param path
+	 *            path of the context; use "/" for the root context if binding
+	 *            to the root is desired.
+	 * @return the context to add servlets into.
+	 * @since 7.0
+	 */
+	public ServletContextHandler addContext(String path) {
+		assertNotYetSetUp();
+		if ("".equals(path))
+			path = "/";
+
+		ServletContextHandler ctx = new ServletContextHandler();
+		ctx.setContextPath(path);
+		contexts.addHandler(ctx);
+
+		return ctx;
+	}
+
+	/**
+	 * Configure basic authentication.
+	 *
+	 * @param ctx
+	 *            servlet context handler
+	 * @param methods
+	 *            the methods
+	 * @return servlet context handler
+	 * @since 7.0
+	 */
+	public ServletContextHandler authBasic(ServletContextHandler ctx,
+			String... methods) {
+		assertNotYetSetUp();
+		configureAuthentication(ctx, methods);
+		return ctx;
+	}
+
+	/**
+	 * Install basic-authentication constraints on the given context.
+	 * <p>
+	 * The Jetty security API used to build the constraints and attach the
+	 * security handler differs between servlet generations, so each concrete
+	 * {@code AppServer} implements this seam for its Jetty flavor.
+	 *
+	 * @param ctx
+	 *            servlet context handler to secure
+	 * @param methods
+	 *            HTTP methods to constrain; empty constrains all methods
+	 */
+	protected abstract void configureAuthentication(ServletContextHandler ctx,
+			String[] methods);
+
+	/** Login service mapping the single test user and role. */
+	protected static class TestMappedLoginService extends AbstractLoginService {
+		private RolePrincipal role;
+
+		protected final Map<String, UserPrincipal> users = new ConcurrentHashMap<>();
+
+		TestMappedLoginService(String role) {
+			this.role = new RolePrincipal(role);
+		}
+
+		@Override
+		protected void doStart() throws Exception {
+			UserPrincipal p = new UserPrincipal(username,
+					new Password(password));
+			users.put(username, p);
+			super.doStart();
+		}
+
+		@Override
+		protected UserPrincipal loadUserInfo(String user) {
+			return users.get(user);
+		}
+
+		@Override
+		protected List<RolePrincipal> loadRoleInfo(UserPrincipal user) {
+			if (users.get(user.getName()) == null) {
+				return null;
+			}
+			return Collections.singletonList(role);
+		}
+	}
+
+	/**
+	 * Start the server on a random local port.
+	 *
+	 * @throws Exception
+	 *             the server cannot be started, testing is not possible.
+	 */
+	public void setUp() throws Exception {
+		RecordingLogger.clear();
+		log.clear();
+		server.start();
+		config.setSecurePort(getSecurePort());
+		if (secureConfig != null) {
+			secureConfig.setSecurePort(getSecurePort());
+		}
+	}
+
+	/**
+	 * Shutdown the server.
+	 *
+	 * @throws Exception
+	 *             the server refuses to halt, or wasn't running.
+	 */
+	public void tearDown() throws Exception {
+		RecordingLogger.clear();
+		log.clear();
+		server.stop();
+		for (File f : filesToDelete) {
+			f.delete();
+		}
+		filesToDelete.clear();
+	}
+
+	/**
+	 * Get the URI to reference this server.
+	 * <p>
+	 * The returned URI includes the proper host name and port number, but does
+	 * not contain a path.
+	 *
+	 * @return URI to reference this server's root context.
+	 */
+	public URI getURI() {
+		assertAlreadySetUp();
+		String host = connector.getHost();
+		if (host.contains(":") && !host.startsWith("["))
+			host = "[" + host + "]";
+		final String uri = "http://" + host + ":" + getPort();
+		try {
+			return new URI(uri);
+		} catch (URISyntaxException e) {
+			throw new RuntimeException("Unexpected URI error on " + uri, e);
+		}
+	}
+
+	/**
+	 * Get port.
+	 *
+	 * @return the local port number the server is listening on.
+	 */
+	public int getPort() {
+		assertAlreadySetUp();
+		return connector.getLocalPort();
+	}
+
+	/**
+	 * Get secure port.
+	 *
+	 * @return the HTTPS port or -1 if not configured.
+	 */
+	public int getSecurePort() {
+		assertAlreadySetUp();
+		return secureConnector != null ? secureConnector.getLocalPort() : -1;
+	}
+
+	/**
+	 * Get requests.
+	 *
+	 * @return all requests since the server was started.
+	 */
+	public List<AccessEvent> getRequests() {
+		return new ArrayList<>(log.getEvents());
+	}
+
+	/**
+	 * Get requests.
+	 *
+	 * @param base
+	 *            base URI used to access the server.
+	 * @param path
+	 *            the path to locate requests for, relative to {@code base}.
+	 * @return all requests which match the given path.
+	 */
+	public List<AccessEvent> getRequests(URIish base, String path) {
+		return getRequests(HttpTestCase.join(base, path));
+	}
+
+	/**
+	 * Get requests.
+	 *
+	 * @param path
+	 *            the path to locate requests for.
+	 * @return all requests which match the given path.
+	 */
+	public List<AccessEvent> getRequests(String path) {
+		ArrayList<AccessEvent> r = new ArrayList<>();
+		for (AccessEvent event : log.getEvents()) {
+			if (event.getPath().equals(path)) {
+				r.add(event);
+			}
+		}
+		return r;
+	}
+
+	private void assertNotYetSetUp() {
+		assertFalse("server is not running", server.isRunning());
+	}
+
+	private void assertAlreadySetUp() {
+		assertTrue("server is running", server.isRunning());
+	}
+}