blob: 9c1475c027eab0295cce3a7c6c56577fbd1d71b1 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed 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 com.google.gwt.dev.shell.jetty;
import com.google.gwt.core.ext.ServletContainer;
import com.google.gwt.core.ext.ServletContainerLauncher;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.util.InstalledHelpInfo;
import com.google.gwt.dev.util.Util;
import com.google.gwt.thirdparty.guava.common.collect.Iterators;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Response;
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.RequestLogHandler;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.ClasspathPattern;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* A {@link ServletContainerLauncher} for an embedded Jetty server.
*/
public class JettyLauncher extends ServletContainerLauncher {
/**
* Log jetty requests/responses to TreeLogger.
*/
public static class JettyRequestLogger extends AbstractLifeCycle implements
RequestLog {
private final TreeLogger logger;
private final TreeLogger.Type normalLogLevel;
public JettyRequestLogger(TreeLogger logger, TreeLogger.Type normalLogLevel) {
this.logger = logger;
assert (normalLogLevel != null);
this.normalLogLevel = normalLogLevel;
}
/**
* Log an HTTP request/response to TreeLogger.
*/
public void log(Request request, Response response) {
int status = response.getStatus();
if (status < 0) {
// Copied from NCSARequestLog
status = 404;
}
TreeLogger.Type logStatus, logHeaders;
if (status >= 500) {
logStatus = TreeLogger.ERROR;
logHeaders = TreeLogger.INFO;
} else if (status == 404) {
if ("/favicon.ico".equals(request.getRequestURI())
&& request.getQueryString() == null) {
/*
* We do not want to call the developer's attention to a 404 when
* requesting favicon.ico. This is a very common 404.
*/
logStatus = TreeLogger.TRACE;
logHeaders = TreeLogger.DEBUG;
} else {
logStatus = TreeLogger.WARN;
logHeaders = TreeLogger.INFO;
}
} else if (status >= 400) {
logStatus = TreeLogger.WARN;
logHeaders = TreeLogger.INFO;
} else {
logStatus = normalLogLevel;
logHeaders = TreeLogger.DEBUG;
}
String userString = request.getRemoteUser();
if (userString == null) {
userString = "";
} else {
userString += "@";
}
String bytesString = "";
if (response.getContentCount() > 0) {
bytesString = " " + response.getContentCount() + " bytes";
}
if (logger.isLoggable(logStatus)) {
TreeLogger branch = logger.branch(logStatus, String.valueOf(status)
+ " - " + request.getMethod() + ' ' + request.getRequestURI() + " ("
+ userString + request.getRemoteHost() + ')' + bytesString);
if (branch.isLoggable(logHeaders)) {
logHeaders(branch.branch(logHeaders, "Request headers"), logHeaders,
request.getHttpFields());
logHeaders(branch.branch(logHeaders, "Response headers"), logHeaders,
response.getHttpFields());
}
}
}
private void logHeaders(TreeLogger logger, TreeLogger.Type logLevel, HttpFields fields) {
for (int i = 0; i < fields.size(); ++i) {
HttpField field = fields.getField(i);
logger.log(logLevel, field.getName() + ": " + field.getValue());
}
}
}
/**
* An adapter for the Jetty logging system to GWT's TreeLogger. This
* implementation class is only public to allow {@link Log} to instantiate it.
*
* The weird static data / default construction setup is a game we play with
* {@link Log}'s static initializer to prevent the initial log message from
* going to stderr.
*/
public static class JettyTreeLogger implements Logger {
private final TreeLogger logger;
public JettyTreeLogger(TreeLogger logger) {
if (logger == null) {
throw new NullPointerException();
}
this.logger = logger;
}
public void debug(String msg, long arg) {
logger.log(TreeLogger.SPAM, format(msg, arg));
}
public void debug(String msg, Object... args) {
if (logger.isLoggable(TreeLogger.SPAM)) {
logger.log(TreeLogger.SPAM, format(msg, args));
}
}
public void debug(String msg, Throwable th) {
logger.log(TreeLogger.SPAM, msg, th);
}
public void debug(Throwable th) {
logger.log(TreeLogger.SPAM, "", th);
}
public Logger getLogger(String name) {
return this;
}
public String getName() {
return "";
}
public void info(String msg, Object... args) {
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, format(msg, args));
}
}
public void info(String msg, Throwable th) {
logger.log(TreeLogger.TRACE, msg, th);
}
public void info(Throwable th) {
logger.log(TreeLogger.TRACE, "", th);
}
public boolean isDebugEnabled() {
return logger.isLoggable(TreeLogger.SPAM);
}
public void setDebugEnabled(boolean enabled) {
// ignored
}
public void warn(String msg, Object... args) {
if (logger.isLoggable(TreeLogger.WARN)) {
logger.log(TreeLogger.WARN, format(msg, args));
}
}
public void warn(String msg, Throwable th) {
logger.log(TreeLogger.WARN, msg, th);
}
public void warn(Throwable th) {
logger.log(TreeLogger.WARN, "", th);
}
public void ignore(Throwable th) {
logger.log(TreeLogger.SPAM, "IGNORE", th);
}
/**
* Copied from org.eclipse.log.StdErrLog.
*/
private String format(String msg, Object... args) {
if (msg == null) {
msg = "";
for (int i = 0; i < args.length; i++) {
msg += "{} ";
}
}
String braces = "{}";
int start = 0;
StringBuilder builder = new StringBuilder();
for (Object arg : args) {
int bracesIndex = msg.indexOf(braces, start);
if (bracesIndex < 0) {
builder.append(msg.substring(start));
builder.append(" ");
builder.append(arg);
start = msg.length();
} else {
builder.append(msg.substring(start, bracesIndex));
builder.append(String.valueOf(arg));
start = bracesIndex + braces.length();
}
}
builder.append(msg.substring(start));
return builder.toString();
}
}
/**
* The resulting {@link ServletContainer} this is launched.
*/
protected static class JettyServletContainer extends ServletContainer {
private final int actualPort;
private final File appRootDir;
private final TreeLogger logger;
private final Server server;
private final WebAppContext wac;
public JettyServletContainer(TreeLogger logger, Server server,
WebAppContext wac, int actualPort, File appRootDir) {
this.logger = logger;
this.server = server;
this.wac = wac;
this.actualPort = actualPort;
this.appRootDir = appRootDir;
}
@Override
public int getPort() {
return actualPort;
}
@Override
public void refresh() throws UnableToCompleteException {
String msg = "Reloading web app to reflect changes in "
+ appRootDir.getAbsolutePath();
TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
// Temporarily log Jetty on the branch.
Log.setLog(new JettyTreeLogger(branch));
try {
server.stop();
server.start();
branch.log(TreeLogger.INFO, "Reload completed successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR, "Unable to restart embedded Jetty server",
e);
throw new UnableToCompleteException();
} finally {
// Reset the top-level logger.
Log.setLog(new JettyTreeLogger(logger));
}
}
@Override
public void stop() throws UnableToCompleteException {
TreeLogger branch = logger.branch(TreeLogger.INFO,
"Stopping Jetty server");
// Temporarily log Jetty on the branch.
Log.setLog(new JettyTreeLogger(branch));
try {
server.stop();
server.setStopAtShutdown(false);
branch.log(TreeLogger.TRACE, "Stopped successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
throw new UnableToCompleteException();
} finally {
// Reset the top-level logger.
Log.setLog(new JettyTreeLogger(logger));
}
}
}
/**
* A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload
* with a new {@link WebAppClassLoader} to pick up disk changes. The default
* Jetty {@code WebAppContext} will create new instances of servlets, but it
* will not create a brand new {@link ClassLoader}. By creating a new {@code
* ClassLoader} each time, we re-read updated classes from disk.
*
* Also provides special class filtering to isolate the web app from the GWT
* hosting environment.
*/
protected static final class WebAppContextWithReload extends WebAppContext {
/**
* Specialized {@link WebAppClassLoader} that allows outside resources to be
* brought in dynamically from the system path. A warning is issued when
* this occurs.
*/
private class WebAppClassLoaderExtension extends WebAppClassLoader {
private static final String META_INF_SERVICES = "META-INF/services/";
private final ClasspathPattern systemClassesFromWebappFirst = new ClasspathPattern(new String[] {
"-javax.servlet.",
"-javax.el.",
"-javax.websocket.",
"javax.",
});
private final ClasspathPattern allowedFromSystemClassLoader = new ClasspathPattern(new String[] {
"org.eclipse.jetty.",
"javax.websocket.",
// Jasper
"org.apache.jasper.",
"org.apache.juli.logging.",
"org.apache.tomcat.",
"org.apache.el.",
// Xerces
"org.apache.xerces.",
"javax.xml.", // Used by Jetty for jetty-web.xml parsing
});
public WebAppClassLoaderExtension() throws IOException {
super(bootStrapOnlyClassLoader, WebAppContextWithReload.this);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// Logic copied from Jetty's WebAppClassLoader, modified to use the system classloader
// instead of the parent classloader for server classes
List<URL> fromParent = WebAppContextWithReload.this.isServerClass(name)
? Collections.<URL>emptyList()
: Lists.newArrayList(Iterators.forEnumeration(systemClassLoader.getResources(name)));
Iterator<URL> fromWebapp = WebAppContextWithReload.this.isSystemClass(name)
&& !fromParent.isEmpty()
? Collections.<URL>emptyIterator()
: Iterators.forEnumeration(findResources(name));
return Iterators.asEnumeration(Iterators.concat(fromWebapp, fromParent.iterator()));
}
@Override
public URL findResource(String name) {
// Specifically for META-INF/services/javax.xml.parsers.SAXParserFactory
String checkName = name;
if (checkName.startsWith(META_INF_SERVICES)) {
checkName = checkName.substring(META_INF_SERVICES.length());
}
checkName = checkName.replace('/', '.');
// For a system path, load from the outside world.
// Note: bootstrap has already been searched, so javax. classes should be
// tried from the webapp first (except for javax.servlet and javax.el).
URL found;
if (WebAppContextWithReload.this.isSystemClass(checkName)
&& !systemClassesFromWebappFirst.match(checkName)) {
found = systemClassLoader.getResource(name);
if (found != null) {
return found;
}
}
// Always check this ClassLoader first.
found = super.findResource(name);
if (found != null) {
return found;
}
// See if the outside world has it.
found = systemClassLoader.getResource(name);
if (found == null || WebAppContextWithReload.this.isServerClass(checkName)) {
return null;
}
// Special-case Jetty/Jasper/etc. resources
if (allowedFromSystemClassLoader.match(checkName) ||
// Jetty-plus reads jndi.properties
"jndi.properties".equals(name)) {
return found;
}
// Warn, add containing URL to our own ClassLoader, and retry the call.
String warnMessage = "Server resource '"
+ name
+ "' could not be found in the web app, but was found on the system classpath";
if (!addContainingClassPathEntry(warnMessage, found, name)) {
return null;
}
return super.findResource(name);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// For system path, always prefer the outside world.
// Note: bootstrap has already been searched, so javax. classes should be
// tried from the webapp first (except for javax.servlet).
if (WebAppContextWithReload.this.isSystemClass(name)
&& !systemClassesFromWebappFirst.match(name)) {
try {
Class<?> loaded = systemClassLoader.loadClass(name);
if (resolve) {
resolveClass(loaded);
}
return loaded;
} catch (ClassNotFoundException e) {
}
}
try {
return super.loadClass(name, resolve);
} catch (ClassNotFoundException e) {
// Don't allow server classes to be loaded from the outside.
if (WebAppContextWithReload.this.isServerClass(name)) {
throw e;
}
}
// See if the outside world has a URL for it.
String resourceName = name.replace('.', '/') + ".class";
URL found = systemClassLoader.getResource(resourceName);
if (found == null) {
throw new ClassNotFoundException(name);
}
// Special-case JDBCUnloader; it should always be loaded in the webapp classloader
if (JDBCUnloader.class.getName().equals(name)) {
byte[] jdbcUnloader = Util.readURLAsBytes(found);
return defineClass(name, jdbcUnloader, 0, jdbcUnloader.length);
}
// Those classes are allowed to be loaded right from the systemClassLoader
// Note: Jetty classes here are not "server classes", handled above.
if (allowedFromSystemClassLoader.match(name)) {
Class<?> loaded = systemClassLoader.loadClass(name);
if (resolve) {
resolveClass(loaded);
}
return loaded;
}
// Warn, add containing URL to our own ClassLoader, and retry the call.
String warnMessage = "Server class '"
+ name
+ "' could not be found in the web app, but was found on the system classpath";
if (!addContainingClassPathEntry(warnMessage, found, resourceName)) {
throw new ClassNotFoundException(name);
}
return super.loadClass(name, resolve);
}
private boolean addContainingClassPathEntry(String warnMessage,
URL resource, String resourceName) {
TreeLogger.Type logLevel = (System.getProperty(PROPERTY_NOWARN_WEBAPP_CLASSPATH) == null)
? TreeLogger.WARN : TreeLogger.DEBUG;
TreeLogger branch = logger.branch(logLevel, warnMessage);
String classPathURL;
String foundStr = resource.toExternalForm();
if (resource.getProtocol().equals("file")) {
assert foundStr.endsWith(resourceName);
classPathURL = foundStr.substring(0, foundStr.length()
- resourceName.length());
} else if (resource.getProtocol().equals("jar")) {
assert foundStr.startsWith("jar:");
assert foundStr.endsWith("!/" + resourceName);
classPathURL = foundStr.substring(4, foundStr.length()
- (2 + resourceName.length()));
} else {
branch.log(TreeLogger.ERROR,
"Found resouce but unrecognized URL format: '" + foundStr + '\'');
return false;
}
branch = branch.branch(logLevel, "Adding classpath entry '"
+ classPathURL + "' to the web app classpath for this session",
null, new InstalledHelpInfo("webAppClassPath.html"));
try {
addClassPath(classPathURL);
return true;
} catch (IOException e) {
branch.log(TreeLogger.ERROR, "Failed add container URL: '"
+ classPathURL + '\'', e);
return false;
}
}
}
/**
* Parent ClassLoader for the Jetty web app, which can only load JVM
* classes. We would just use <code>null</code> for the parent ClassLoader
* except this makes Jetty unhappy.
*/
private final ClassLoader bootStrapOnlyClassLoader = new ClassLoader(null) {
};
private final TreeLogger logger;
/**
* In the usual case of launching {@link com.google.gwt.dev.DevMode}, this
* will always by the system app ClassLoader.
*/
private final ClassLoader systemClassLoader = Thread.currentThread().getContextClassLoader();
private WebAppContextWithReload(TreeLogger logger, String webApp,
String contextPath) {
super(null, contextPath, null, null, null, new ErrorPageErrorHandler(),
ServletContextHandler.SESSIONS);
this.setWar(webApp);
this.logger = logger;
// Prevent file locking on Windows; pick up file changes.
getInitParams().put(
"org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false");
// Since the parent class loader is bootstrap-only, prefer it first.
setParentLoaderPriority(true);
}
@Override
protected void doStart() throws Exception {
setClassLoader(new WebAppClassLoaderExtension());
super.doStart();
}
@Override
protected void doStop() throws Exception {
super.doStop();
Class<?> jdbcUnloader =
getClassLoader().loadClass("com.google.gwt.dev.shell.jetty.JDBCUnloader");
java.lang.reflect.Method unload = jdbcUnloader.getMethod("unload");
unload.invoke(null);
setClassLoader(null);
}
}
/**
* Represents the type of SSL client certificate authentication desired.
*/
private enum ClientAuth {
NONE,
WANT,
REQUIRE,
}
/**
* System property to suppress warnings about loading web app classes from the
* system classpath.
*/
private static final String PROPERTY_NOWARN_WEBAPP_CLASSPATH = "gwt.nowarn.webapp.classpath";
/**
* Setup a connector for the bind address/port.
*
* @param connector
* @param bindAddress
* @param port
*/
private static void setupConnector(ServerConnector connector,
String bindAddress, int port) {
if (bindAddress != null) {
connector.setHost(bindAddress.toString());
}
connector.setPort(port);
// Allow binding to a port even if it's still in state TIME_WAIT.
connector.setReuseAddress(true);
}
// default value used if setBaseLogLevel isn't called
private TreeLogger.Type baseLogLevel = TreeLogger.INFO;
private String bindAddress = null;
private ClientAuth clientAuth;
private String keyStore;
private String keyStorePassword;
private final Object privateInstanceLock = new Object();
private boolean useSsl;
@Override
public String getName() {
return "Jetty";
}
@Override
public boolean isSecure() {
return useSsl;
}
@Override
public boolean processArguments(TreeLogger logger, String arguments) {
if (arguments != null && arguments.length() > 0) {
// TODO(jat): better parsing of the args
for (String arg : arguments.split(",")) {
int equals = arg.indexOf('=');
String tag;
String value = null;
if (equals < 0) {
tag = arg;
} else {
tag = arg.substring(0, equals);
value = arg.substring(equals + 1);
}
if ("ssl".equals(tag)) {
useSsl = true;
URL keyStoreUrl = getClass().getResource("localhost.keystore");
if (keyStoreUrl == null) {
logger.log(TreeLogger.ERROR, "Default GWT keystore not found");
return false;
}
keyStore = keyStoreUrl.toExternalForm();
keyStorePassword = "localhost";
} else if ("keystore".equals(tag)) {
useSsl = true;
keyStore = value;
} else if ("password".equals(tag)) {
useSsl = true;
keyStorePassword = value;
} else if ("pwfile".equals(tag)) {
useSsl = true;
keyStorePassword = Util.readFileAsString(new File(value));
if (keyStorePassword == null) {
logger.log(TreeLogger.ERROR,
"Unable to read keystore password from '" + value + "'");
return false;
}
keyStorePassword = keyStorePassword.trim();
} else if ("clientAuth".equals(tag)) {
useSsl = true;
try {
clientAuth = ClientAuth.valueOf(value);
} catch (IllegalArgumentException e) {
logger.log(TreeLogger.WARN, "Ignoring invalid clientAuth of '"
+ value + "'");
}
} else {
logger.log(TreeLogger.ERROR, "Unexpected argument to "
+ JettyLauncher.class.getSimpleName() + ": " + arg);
return false;
}
}
if (useSsl) {
if (keyStore == null) {
logger.log(TreeLogger.ERROR, "A keystore is required to use SSL");
return false;
}
if (keyStorePassword == null) {
logger.log(TreeLogger.ERROR,
"A keystore password is required to use SSL");
return false;
}
}
}
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.
*/
public void setBaseRequestLogLevel(TreeLogger.Type baseLogLevel) {
synchronized (privateInstanceLock) {
this.baseLogLevel = baseLogLevel;
}
}
@Override
public void setBindAddress(String bindAddress) {
this.bindAddress = bindAddress;
}
@Override
public ServletContainer start(TreeLogger logger, int port, File appRootDir)
throws Exception {
TreeLogger branch = logger.branch(TreeLogger.TRACE,
"Starting Jetty on port " + port, null);
checkStartParams(branch, port, appRootDir);
// Setup our branch logger during startup.
Log.setLog(new JettyTreeLogger(branch));
// Force load some JRE singletons that can pin the classloader.
jreLeakPrevention(logger);
// Turn off XML validation.
System.setProperty("org.eclipse.jetty.xml.XmlParser.Validating", "false");
Server server = new Server();
ServerConnector connector = getConnector(server, logger);
setupConnector(connector, bindAddress, port);
server.addConnector(connector);
Configuration.ClassList cl = Configuration.ClassList.setServerDefault(server);
try {
// from jetty-plus.xml
Thread.currentThread().getContextClassLoader().loadClass("org.eclipse.jetty.plus.webapp.PlusConfiguration");
cl.addAfter("org.eclipse.jetty.webapp.FragmentConfiguration",
"org.eclipse.jetty.plus.webapp.EnvConfiguration",
"org.eclipse.jetty.plus.webapp.PlusConfiguration");
} catch (ClassNotFoundException cnfe) {
logger.log(TreeLogger.Type.DEBUG, "jetty-plus isn't on the classpath, JNDI won't work. This might also affect annotations scanning and JSP.");
}
try {
// from jetty-annotations.xml
Thread.currentThread().getContextClassLoader()
.loadClass("org.eclipse.jetty.annotations.AnnotationConfiguration");
cl.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
"org.eclipse.jetty.annotations.AnnotationConfiguration");
} catch (ClassNotFoundException cnfe) {
logger.log(TreeLogger.Type.DEBUG, "jetty-annotations isn't on the classpath, annotation scanning won't work. This might also affect annotations scanning.");
}
// Create a new web app in the war directory.
WebAppContext wac = createWebAppContext(logger, appRootDir);
wac.setSecurityHandler(new ConstraintSecurityHandler());
RequestLogHandler logHandler = new RequestLogHandler();
logHandler.setRequestLog(new JettyRequestLogger(logger, getBaseLogLevel()));
logHandler.setHandler(wac);
server.setHandler(logHandler);
server.start();
server.setStopAtShutdown(true);
// Now that we're started, log to the top level logger.
Log.setLog(new JettyTreeLogger(logger));
// DevMode#doStartUpServer() fails from time to time (rarely) due
// to an unknown error. Adding some logging to pinpoint the problem.
int connectorPort = connector.getLocalPort();
if (connector.getLocalPort() < 0) {
branch.log(TreeLogger.ERROR, String.format(
"Failed to connect to open channel with port %d (return value %d)",
port, connectorPort));
}
return createServletContainer(logger, appRootDir, server, wac,
connectorPort);
}
protected JettyServletContainer createServletContainer(TreeLogger logger,
File appRootDir, Server server, WebAppContext wac, int localPort) {
return new JettyServletContainer(logger, server, wac, localPort, appRootDir);
}
protected WebAppContext createWebAppContext(TreeLogger logger, File appRootDir) {
WebAppContext context = new WebAppContextWithReload(logger, appRootDir.getAbsolutePath(), "/");
context.setConfigurationClasses(new String[] {
"org.eclipse.jetty.webapp.WebInfConfiguration",
"org.eclipse.jetty.webapp.WebXmlConfiguration",
"org.eclipse.jetty.webapp.MetaInfConfiguration",
"org.eclipse.jetty.webapp.FragmentConfiguration",
"org.eclipse.jetty.plus.webapp.EnvConfiguration",
"org.eclipse.jetty.plus.webapp.PlusConfiguration",
"org.eclipse.jetty.annotations.AnnotationConfiguration",
"org.eclipse.jetty.webapp.JettyWebXmlConfiguration"
});
return context;
}
protected ServerConnector getConnector(Server server, TreeLogger logger) {
HttpConfiguration config = defaultConfig();
if (useSsl) {
TreeLogger sslLogger = logger.branch(TreeLogger.INFO,
"Listening for SSL connections");
if (sslLogger.isLoggable(TreeLogger.TRACE)) {
sslLogger.log(TreeLogger.TRACE, "Using keystore " + keyStore);
}
SslContextFactory ssl = new SslContextFactory();
if (clientAuth != null) {
switch (clientAuth) {
case NONE:
ssl.setWantClientAuth(false);
ssl.setNeedClientAuth(false);
break;
case WANT:
sslLogger.log(TreeLogger.TRACE, "Requesting client certificates");
ssl.setWantClientAuth(true);
ssl.setNeedClientAuth(false);
break;
case REQUIRE:
sslLogger.log(TreeLogger.TRACE, "Requiring client certificates");
ssl.setWantClientAuth(true);
ssl.setNeedClientAuth(true);
break;
}
}
ssl.setKeyStorePath(keyStore);
ssl.setTrustStorePath(keyStore);
ssl.setKeyStorePassword(keyStorePassword);
ssl.setTrustStorePassword(keyStorePassword);
config.addCustomizer(new SecureRequestCustomizer());
return new ServerConnector(server,
null, null, null, 0, 2,
new SslConnectionFactory(ssl, "http/1.1"),
new HttpConnectionFactory(config));
}
return new ServerConnector(server, new HttpConnectionFactory(config));
}
protected HttpConfiguration defaultConfig() {
HttpConfiguration config = new HttpConfiguration();
config.setRequestHeaderSize(16386);
config.setSendServerVersion(false);
config.setSendDateHeader(true);
return config;
}
private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
if (logger == null) {
throw new NullPointerException("logger cannot be null");
}
if (port < 0 || port > 65535) {
throw new IllegalArgumentException(
"port must be either 0 (for auto) or less than 65536");
}
if (appRootDir == null) {
throw new NullPointerException("app root direcotry cannot be null");
}
}
/*
* 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
*
* http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/core/
* JreMemoryLeakPreventionListener.java
*
* Relevant part of the Tomcat NOTICE, retrieved from
* http://svn.apache.org/repos/asf/tomcat/trunk/NOTICE Apache Tomcat Copyright
* 1999-2010 The Apache Software Foundation
*
* This product includes software developed by The Apache Software Foundation
* (http://www.apache.org/).
*/
private void jreLeakPrevention(TreeLogger logger) {
// Trigger a call to sun.awt.AppContext.getAppContext(). This will
// pin the common class loader in memory but that shouldn't be an
// issue.
ImageIO.getCacheDirectory();
/*
* Several components end up calling: sun.misc.GC.requestLatency(long)
*
* Those libraries / components known to trigger memory leaks due to
* eventual calls to requestLatency(long) are: -
* javax.management.remote.rmi.RMIConnectorServer.start()
*/
try {
Class<?> clazz = Class.forName("sun.misc.GC");
Method method = clazz.getDeclaredMethod("requestLatency",
new Class[]{long.class});
method.invoke(null, Long.valueOf(3600000));
} catch (ClassNotFoundException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.gcDaemonFail", e);
} catch (SecurityException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.gcDaemonFail", e);
} catch (NoSuchMethodException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.gcDaemonFail", e);
} catch (IllegalArgumentException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.gcDaemonFail", e);
} catch (IllegalAccessException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.gcDaemonFail", e);
} catch (InvocationTargetException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.gcDaemonFail", e);
}
/*
* Calling getPolicy retains a static reference to the context class loader.
*/
try {
// Policy.getPolicy();
Class<?> policyClass = Class.forName("javax.security.auth.Policy");
Method method = policyClass.getMethod("getPolicy");
method.invoke(null);
} catch (ClassNotFoundException e) {
// Ignore. The class is deprecated.
} catch (SecurityException e) {
// Ignore. Don't need call to getPolicy() to be successful,
// just need to trigger static initializer.
} catch (NoSuchMethodException e) {
logger.log(TreeLogger.WARN, "jreLeakPrevention.authPolicyFail", e);
} catch (IllegalArgumentException e) {
logger.log(TreeLogger.WARN, "jreLeakPrevention.authPolicyFail", e);
} catch (IllegalAccessException e) {
logger.log(TreeLogger.WARN, "jreLeakPrevention.authPolicyFail", e);
} catch (InvocationTargetException e) {
logger.log(TreeLogger.WARN, "jreLeakPrevention.authPolicyFail", e);
}
/*
* Creating a MessageDigest during web application startup initializes the
* Java Cryptography Architecture. Under certain conditions this starts a
* Token poller thread with TCCL equal to the web application class loader.
*
* Instead we initialize JCA right now.
*/
java.security.Security.getProviders();
/*
* Several components end up opening JarURLConnections without first
* disabling caching. This effectively locks the file. Whilst more
* noticeable and harder to ignore on Windows, it affects all operating
* systems.
*
* Those libraries/components known to trigger this issue include: - log4j
* versions 1.2.15 and earlier - javax.xml.bind.JAXBContext.newInstance()
*/
// Set the default URL caching policy to not to cache
try {
// Doesn't matter that this JAR doesn't exist - just as long as
// the URL is well-formed
URL url = new URL("jar:file://dummy.jar!/");
URLConnection uConn = url.openConnection();
uConn.setDefaultUseCaches(false);
} catch (MalformedURLException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.jarUrlConnCacheFail", e);
} catch (IOException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.jarUrlConnCacheFail", e);
}
/*
* Haven't got to the root of what is going on with this leak but if a web
* app is the first to make the calls below the web application class loader
* will be pinned in memory.
*/
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
logger.log(TreeLogger.ERROR, "jreLeakPrevention.xmlParseFail", e);
}
}
}