diff --git a/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java b/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
index 9c7602a09f..14148090de 100644
--- a/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
+++ b/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
@@ -17,6 +17,7 @@
import java.io.File;
import java.net.BindException;
+import java.util.regex.Pattern;
/**
* Defines the service provider interface for launching servlet containers that
@@ -25,8 +26,21 @@
* Subclasses should be careful about calling any methods defined on this class
* or else they risk failing when used with a version of GWT that did not have
* those methods.
+ *
+ * As of GWT 2.13, launcher implementations can be discovered by a service loader. Launchers that
+ * specify a name can be selected by the user via the {@code -server} argument to DevMode using
+ * that name instead of their fully qualified class name. Additionally, if only one launcher type
+ * is present on the classpath, it will be used automatically without the need to specify it. As a
+ * result, names should be unique, and projects may wish to take care to avoid allowing more than
+ * one provider at a time on the classpath.
*/
public abstract class ServletContainerLauncher {
+ /**
+ * Allowed names for ServletContainerLauncher instances, to be able to be used with a
+ * ServiceLoader. If not registered as a service, the "-server" argument can be used with the
+ * class's fully qualified name, and the name property need not follow this pattern.
+ */
+ public static final Pattern SERVICE_NAME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9_$.]+");
/*
* NOTE: Any new methods must have default implementations, and any users of
* this class must be prepared to handle LinkageErrors when calling new
@@ -46,13 +60,14 @@ public byte[] getIconBytes() {
* if no name should be displayed.
*/
public String getName() {
- return "Web Server";
+ return "Default Web Server";
}
+
/**
* Return true if this servlet container launcher is configured for secure
* operation (ie, HTTPS). This value is only queried after arguments, if any,
* have been processed.
- *
+ *
* The default implementation just returns false.
*
* @return true if HTTPS is in use
@@ -76,6 +91,17 @@ public boolean processArguments(TreeLogger logger, String arguments) {
return false;
}
+ /**
+ * Specifies the default log level. Presently DevMode (and JUnitShell) will set this to TRACE
+ * when using a RemoteUI implementation, INFO for other implementations.
+ *
+ * Default implementation does nothing, subclasses are encouraged to use this to configure their
+ * own logggers.
+ */
+ public void setBaseRequestLogLevel(TreeLogger.Type baseLogLevel) {
+ // Do nothing by default.
+ }
+
/**
* Set the bind address for the web server socket.
*
diff --git a/dev/core/src/com/google/gwt/dev/DevMode.java b/dev/core/src/com/google/gwt/dev/DevMode.java
index 2d1cbc0cba..8469d964e1 100644
--- a/dev/core/src/com/google/gwt/dev/DevMode.java
+++ b/dev/core/src/com/google/gwt/dev/DevMode.java
@@ -26,8 +26,8 @@
import com.google.gwt.dev.shell.BrowserListener;
import com.google.gwt.dev.shell.CodeServerListener;
import com.google.gwt.dev.shell.OophmSessionHandler;
+import com.google.gwt.dev.shell.StaticResourceServer;
import com.google.gwt.dev.shell.SuperDevListener;
-import com.google.gwt.dev.shell.jetty.JettyLauncher;
import com.google.gwt.dev.ui.RestartServerCallback;
import com.google.gwt.dev.ui.RestartServerEvent;
import com.google.gwt.dev.util.InstalledHelpInfo;
@@ -56,12 +56,15 @@
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
import java.net.BindException;
import java.net.URL;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
/**
* The main executable class for the hosted mode shell. NOTE: the public API for
@@ -122,16 +125,32 @@ public boolean setFlag(boolean value) {
}
/**
- * Handles the -server command line flag.
+ * Handles the -server command line flag. If unspecified, tries to find a single SCL defined in
+ * the service loader, or else defaults to StaticResourceServer.
*/
protected static class ArgHandlerServer extends ArgHandlerString {
- private static final String DEFAULT_SCL = JettyLauncher.class.getName();
-
- private HostedModeOptions options;
+ private static final String DEFAULT_SCL = StaticResourceServer.class.getName();
+ private final HostedModeOptions options;
+ private final Map registered;
public ArgHandlerServer(HostedModeOptions options) {
this.options = options;
+ registered = ServiceLoader.load(ServletContainerLauncher.class).stream()
+ .map(ServiceLoader.Provider::get)
+ .filter(scl -> {
+ if (!ServletContainerLauncher.SERVICE_NAME_PATTERN.matcher(scl.getName()).matches()) {
+ System.err.println("Server class '" + scl.getClass().getName() +
+ "' has an invalid name '" + scl.getName() +
+ "'. To be used from the service loader, this name must match " +
+ ServletContainerLauncher.SERVICE_NAME_PATTERN.pattern() + ". Skipping.");
+ return false;
+ }
+ return true;
+ })
+ .collect(Collectors.toMap(
+ ServletContainerLauncher::getName,
+ scl -> scl));
}
@Override
@@ -139,7 +158,15 @@ public String[] getDefaultArgs() {
if (options.isNoServer()) {
return null;
} else {
- return new String[] {getTag(), DEFAULT_SCL};
+ if (registered.size() == 1) {
+ // Exactly one registered SCL, use it as the default, by fully qualified class name
+ return new String[] {
+ getTag(),
+ registered.values().iterator().next().getClass().getName()
+ };
+ }
+ // Use the default SCL
+ return new String[] { getTag(), DEFAULT_SCL };
}
}
@@ -162,38 +189,50 @@ public String[] getTagArgs() {
public boolean setString(String arg) {
// Supercedes -noserver.
options.setNoServer(false);
- String sclClassName;
- String sclArgs;
+ String sclName;
int idx = arg.indexOf(':');
if (idx >= 0) {
- sclArgs = arg.substring(idx + 1);
- sclClassName = arg.substring(0, idx);
+ options.setServletContainerLauncherArgs(arg.substring(idx + 1));
+ sclName = arg.substring(0, idx);
} else {
- sclArgs = null;
- sclClassName = arg;
+ sclName = arg;
}
- if (sclClassName.length() == 0) {
- sclClassName = DEFAULT_SCL;
+ if (sclName.isEmpty()) {
+ sclName = DEFAULT_SCL;
}
+ // Try to load the class by name
Throwable t;
try {
Class> clazz =
- Class.forName(sclClassName, true, Thread.currentThread().getContextClassLoader());
+ Class.forName(sclName, true, Thread.currentThread().getContextClassLoader());
Class extends ServletContainerLauncher> sclClass =
clazz.asSubclass(ServletContainerLauncher.class);
- options.setServletContainerLauncher(sclClass.newInstance());
- options.setServletContainerLauncherArgs(sclArgs);
+ options.setServletContainerLauncher(sclClass.getDeclaredConstructor().newInstance());
return true;
- } catch (ClassCastException e) {
- t = e;
- } catch (ClassNotFoundException e) {
- t = e;
- } catch (InstantiationException e) {
- t = e;
- } catch (IllegalAccessException e) {
+ } catch (ClassCastException | ClassNotFoundException | InstantiationException |
+ IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+ // Don't log any error until we've tried the service loader too
t = e;
}
- System.err.println("Unable to load server class '" + sclClassName + "'");
+
+ if (registered.containsKey(sclName)) {
+ options.setServletContainerLauncher(registered.get(sclName));
+ return true;
+ }
+ System.err.println("Failed to find a server class with name '" + sclName +
+ "' in the service loader:");
+ if (registered.isEmpty()) {
+ System.err.println("No server classes found in the service loader.");
+ } else {
+ System.err.println("Available server classes:");
+ for (ServletContainerLauncher servletContainerLauncher : registered.values()) {
+ System.err.println(" * " + servletContainerLauncher.getName() + " - " +
+ servletContainerLauncher.getClass().getName());
+ }
+ }
+
+ System.err.println("Unable to load server class '" + sclName +
+ "' by fully qualified name or from the service loader");
t.printStackTrace();
return false;
}
@@ -618,14 +657,7 @@ protected int doStartUpServer() {
ui.setWebServerSecure(serverLogger);
}
- /*
- * TODO: This is a hack to pass the base log level to the SCL. We'll have
- * to figure out a better way to do this for SCLs in general.
- */
- if (scl instanceof JettyLauncher) {
- JettyLauncher jetty = (JettyLauncher) scl;
- jetty.setBaseRequestLogLevel(getBaseLogLevelForUI());
- }
+ scl.setBaseRequestLogLevel(getBaseLogLevelForUI());
scl.setBindAddress(options.getBindAddress());
if (serverLogger.isLoggable(TreeLogger.TRACE)) {
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
index c5d3cbcccd..78001f61a8 100644
--- a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -89,10 +89,9 @@ public static void suppressDeprecationWarningForTests() {
*/
private static void maybeLogDeprecationWarning(TreeLogger log) {
if (hasLoggedDeprecationWarning.compareAndSet(false, true)) {
- log.log(TreeLogger.Type.WARN, "DevMode will default to -noserver in a future release, and " +
- "JettyLauncher may be removed or changed. Please consider running your own " +
- "application server and either passing -noserver to DevMode or migrating to " +
- "CodeServer. Alternatively, consider implementing your own " +
+ log.log(TreeLogger.Type.WARN, "JettyLauncher is deprecated for removal. Please consider" +
+ "running your own application server and either passing -noserver to DevMode or " +
+ "migrating to CodeServer. Alternatively, consider implementing your own " +
"ServletContainerLauncher to continue running your application server from " +
"DevMode.");
}
@@ -543,12 +542,9 @@ private static void setupConnector(ServerConnector connector,
private SslConfiguration sslConfig = new SslConfiguration(ClientAuthType.NONE, null, null, false);
- private final Object privateInstanceLock = new Object();
-
-
@Override
public String getName() {
- return "Jetty";
+ return "DeprecatedJettyLauncher";
}
@Override
@@ -569,15 +565,9 @@ public boolean processArguments(TreeLogger logger, String arguments) {
return true;
}
- /*
- * TODO: This is a hack to pass the base log level to the SCL. We'll have to
- * figure out a better way to do this for SCLs in general. Please do not
- * depend on this method, as it is subject to change.
- */
+ @Override
public void setBaseRequestLogLevel(TreeLogger.Type baseLogLevel) {
- synchronized (privateInstanceLock) {
- this.baseLogLevel = baseLogLevel;
- }
+ this.baseLogLevel = baseLogLevel;
}
@Override
@@ -634,7 +624,7 @@ public ServletContainer start(TreeLogger logger, int port, File appRootDir)
wac.setSecurityHandler(new ConstraintSecurityHandler());
RequestLogHandler logHandler = new RequestLogHandler();
- logHandler.setRequestLog(new JettyRequestLogger(logger, getBaseLogLevel()));
+ logHandler.setRequestLog(new JettyRequestLogger(logger, this.baseLogLevel));
logHandler.setHandler(wac);
server.setHandler(logHandler);
server.start();
@@ -725,16 +715,6 @@ private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
}
}
- /*
- * TODO: This is a hack to pass the base log level to the SCL. We'll have to
- * figure out a better way to do this for SCLs in general.
- */
- private TreeLogger.Type getBaseLogLevel() {
- synchronized (privateInstanceLock) {
- return this.baseLogLevel;
- }
- }
-
/**
* This is a modified version of JreMemoryLeakPreventionListener.java found
* in the Apache Tomcat project at
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/SslConfiguration.java b/dev/core/src/com/google/gwt/dev/shell/jetty/SslConfiguration.java
index 3d6da8ef1a..3eefbcb0b3 100644
--- a/dev/core/src/com/google/gwt/dev/shell/jetty/SslConfiguration.java
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/SslConfiguration.java
@@ -54,7 +54,7 @@ public static Optional parseArgs(String[] args, TreeLogger log
}
if ("ssl".equals(tag)) {
useSsl = true;
- URL keyStoreUrl = JettyLauncher.class.getResource("localhost.keystore");
+ URL keyStoreUrl = SslConfiguration.class.getResource("localhost.keystore");
if (keyStoreUrl == null) {
logger.log(TreeLogger.ERROR, "Default GWT keystore not found");
return Optional.empty();
@@ -85,8 +85,7 @@ public static Optional parseArgs(String[] args, TreeLogger log
+ value + "'");
}
} else {
- logger.log(TreeLogger.ERROR, "Unexpected argument to "
- + JettyLauncher.class.getSimpleName() + ": " + arg);
+ logger.log(TreeLogger.ERROR, "Unexpected SSL argument: " + arg);
return Optional.empty();
}
}