package com.xoom.oss.feathercon;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
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.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Collection;
import java.util.EventListener;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class FeatherCon {
public static final Integer DEFAULT_PORT = 8080;
public final Integer port;
public final String contextName;
public final Map<String, Object> servletContextAttributes;
private final Server server;
private FeatherCon(Server server, Integer port, String contextName, Map<String, Object> servletContextAttributes) {
this.server = server;
this.port = port;
this.contextName = contextName;
this.servletContextAttributes = servletContextAttributes;
}
public void start() throws Exception {
server.start();
}
public void stop() throws Exception {
server.stop();
}
public boolean isRunning() {
return server.isRunning();
}
public void join() throws InterruptedException {
server.join();
}
/**
* Return the port of the first connector supported by HttpConnectionFactory. When using an ephemeral port,
* this value will be nonzero, unlike the value of this.port.
*
* @return http port
*/
public Integer getHttpPort() {
return getPort(Scheme.HTTP);
}
/**
* Return the port of the first connector supported by SslConnectionFactory. When using an ephemeral port,
* this value will be nonzero, unlike the value of this.port.
*
* @return https port
*/
public Integer getHttpsPort() {
return getPort(Scheme.HTTPS);
}
private Integer getPort(Scheme scheme) {
for (Connector connector : server.getConnectors()) {
if (connector instanceof ServerConnector) {
ServerConnector serverConnector = (ServerConnector) connector;
Collection<ConnectionFactory> connectionFactories = serverConnector.getConnectionFactories();
for (ConnectionFactory connectionFactory : connectionFactories) {
Class<? extends AbstractConnectionFactory> connectorClass;
switch (scheme) {
case HTTP:
connectorClass = HttpConnectionFactory.class;
break;
case HTTPS:
connectorClass = SslConnectionFactory.class;
break;
default:
throw new UnsupportedOperationException("No such scheme.");
}
if (connectorClass.isAssignableFrom(connectionFactory.getClass())) {
return serverConnector.getLocalPort();
}
}
}
}
return null;
}
public static class Builder {
private final Logger logger = LoggerFactory.getLogger(Builder.class);
protected Integer port = DEFAULT_PORT;
protected String contextName = "/";
protected Map<String, Object> servletContextAttributes = new HashMap<String, Object>();
protected List<EventListener> servletContextListeners = new LinkedList<EventListener>();
protected List<FilterWrapper> filters = new LinkedList<FilterWrapper>();
protected List<ServletConfiguration> servletConfigurations = new LinkedList<ServletConfiguration>();
protected Map<String, String> initParameters = new HashMap<String, String>();
protected SSLConfiguration sslConfiguration;
protected WebSocketEndpointConfiguration webSocketConfiguration;
private Boolean built = false;
protected Builder bindAll(Builder builder) {
this.port = builder.port;
this.contextName = builder.contextName;
this.servletContextAttributes = builder.servletContextAttributes;
this.servletContextListeners = builder.servletContextListeners;
this.filters = builder.filters;
this.servletConfigurations = builder.servletConfigurations;
this.initParameters.putAll(builder.initParameters);
return this;
}
public Builder withPort(Integer port) {
this.port = port;
return this;
}
public Builder withServletContextAttribute(String key, Object value) {
servletContextAttributes.put(key, value);
return this;
}
public Builder withServletContextListener(EventListener servletContextListener) {
servletContextListeners.add(servletContextListener);
return this;
}
public Builder withInitParameter(String key, String value) {
initParameters.put(key, value);
return this;
}
public Builder withServletContextListener(String servletContextListenerClassName) {
try {
EventListener aClass = (EventListener) getClass().getClassLoader().loadClass(servletContextListenerClassName).newInstance();
servletContextListeners.add(aClass);
} catch (ClassNotFoundException e) {
throw new RuntimeException(String.format("Cannot load servlet class %s", servletContextListenerClassName), e);
} catch (InstantiationException e) {
throw new RuntimeException(String.format("Cannot load servlet class %s", servletContextListenerClassName), e);
} catch (IllegalAccessException e) {
throw new RuntimeException(String.format("Cannot load servlet class %s", servletContextListenerClassName), e);
}
return this;
}
public Builder withFilter(FilterWrapper filterWrapper) {
filters.add(filterWrapper);
return this;
}
public Builder withServletConfiguration(ServletConfiguration servletConfiguration) {
servletConfigurations.add(servletConfiguration);
return this;
}
public Builder withContextName(String contextName) {
this.contextName = contextName;
return this;
}
public Builder withSslConfiguration(SSLConfiguration sslConfiguration) {
this.sslConfiguration = sslConfiguration;
return this;
}
public Builder withWebSocketConfiguration(WebSocketEndpointConfiguration webSocketConfiguration) {
this.webSocketConfiguration = webSocketConfiguration;
return this;
}
public FeatherCon build() {
if (built) {
throw new IllegalStateException("This builder can be used to produce one server instance. Please create a new builder.");
}
Server server = new Server();
port = port == null ? DEFAULT_PORT : port;
ServletContextHandler contextHandler = new ServletContextHandler(server, contextName);
for (ServletConfiguration servletConfiguration : servletConfigurations) {
for (String pathSpec : servletConfiguration.pathSpecs) {
contextHandler.addServlet(servletConfiguration.servletHolder, pathSpec);
}
}
HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[]{contextHandler});
server.setHandler(handlers);
for (String key : initParameters.keySet()) {
contextHandler.setInitParameter(key, initParameters.get(key));
}
for (String key : servletContextAttributes.keySet()) {
contextHandler.getServletContext().setAttribute(key, servletContextAttributes.get(key));
}
for (EventListener eventListener : servletContextListeners) {
contextHandler.addEventListener(eventListener);
}
for (FilterWrapper filterBuffer : filters) {
for (String pathSpec : filterBuffer.pathSpec) {
contextHandler.addFilter(filterBuffer.filterHolder, pathSpec, filterBuffer.dispatches);
}
}
if (webSocketConfiguration != null) {
ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(contextHandler);
for (Class c : webSocketConfiguration.endpointClasses) {
try {
wscontainer.addEndpoint(c);
} catch (DeploymentException e) {
throw new RuntimeException(e);
}
}
for (ServerEndpointConfig serverEndpointConfig : webSocketConfiguration.serverEndpointConfigs) {
try {
wscontainer.addEndpoint(serverEndpointConfig);
} catch (DeploymentException e) {
throw new RuntimeException(e);
}
}
}
// Jetty 9 connector config: http://goo.gl/26lM7
HttpConfiguration http_config = new HttpConfiguration();
http_config.setOutputBufferSize(32768);
if (sslConfiguration == null || !sslConfiguration.sslOnly) {
ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config));
http.setPort(port);
http.setIdleTimeout(30000);
server.addConnector(http);
}
if (sslConfiguration != null) {
http_config.setSecureScheme("https");
HttpConfiguration https_config = new HttpConfiguration(http_config);
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
https_config.addCustomizer(customizer);
// exclude cipher suites: https://jira.atlassian.com/browse/CRUC-6594
// http://www.eclipse.org/jetty/documentation/current/configuring-ssl.html
SslContextFactory sslContextFactory = new SslContextFactory(sslConfiguration.keyStoreFile.getAbsolutePath());
sslContextFactory.setExcludeCipherSuites(sslConfiguration.excludeCipherSuites.toArray(new String[sslConfiguration.excludeCipherSuites.size()]));
sslContextFactory.setKeyStorePassword(sslConfiguration.keyStorePassword);
ServerConnector https = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"));
https.setPort(sslConfiguration.sslPort);
https.setIdleTimeout(500000);
server.addConnector(https);
}
FeatherCon featherCon = new FeatherCon(server, port, contextName, servletContextAttributes);
built = true;
logger.info("Built {}", this);
return featherCon;
}
@Override
public String toString() {
return "FeatherConBuilder{" +
"port=" + port +
", contextName='" + contextName + '\'' +
", servletContextAttributes=" + servletContextAttributes +
", servletContextListeners=" + servletContextListeners +
", filters=" + filters +
", servletConfigurations=" + servletConfigurations + '}';
}
}
@Override
public String toString() {
return "FeatherCon{" +
"port=" + port +
", contextName='" + contextName + '\'' +
", servletContextAttributes=" + servletContextAttributes +
", server=" + server +
'}';
}
}