package io.dropwizard.server;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.codahale.metrics.jetty9.InstrumentedHandler;
import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool;
import com.codahale.metrics.servlets.AdminServlet;
import com.codahale.metrics.servlets.HealthCheckServlet;
import com.codahale.metrics.servlets.MetricsServlet;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import com.google.common.io.Resources;
import io.dropwizard.jersey.filter.AllowedMethodsFilter;
import io.dropwizard.jersey.jackson.JacksonBinder;
import io.dropwizard.jersey.setup.JerseyEnvironment;
import io.dropwizard.jersey.validation.HibernateValidationFeature;
import io.dropwizard.jetty.GzipHandlerFactory;
import io.dropwizard.jetty.MutableServletContextHandler;
import io.dropwizard.jetty.NonblockingServletHolder;
import io.dropwizard.jetty.ServerPushFilterFactory;
import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
import io.dropwizard.request.logging.LogbackAccessRequestLogFactory;
import io.dropwizard.request.logging.RequestLogFactory;
import io.dropwizard.servlets.ThreadNameFilter;
import io.dropwizard.setup.ExceptionMapperBinder;
import io.dropwizard.util.Duration;
import io.dropwizard.validation.MinDuration;
import io.dropwizard.validation.ValidationMethod;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.setuid.RLimit;
import org.eclipse.jetty.setuid.SetUIDListener;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.regex.Pattern;
public abstract class AbstractServerFactory implements ServerFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(ServerFactory.class);
private static final Pattern WINDOWS_NEWLINE = Pattern.compile("\\r\\n?");
@Valid
@Nullable
private RequestLogFactory requestLog;
@Valid
@NotNull
private GzipHandlerFactory gzip = new GzipHandlerFactory();
@Valid
@NotNull
private ServerPushFilterFactory serverPush = new ServerPushFilterFactory();
@Min(2)
private int maxThreads = 1024;
@Min(1)
private int minThreads = 8;
private int maxQueuedRequests = 1024;
@MinDuration(1)
private Duration idleThreadTimeout = Duration.minutes(1);
@Min(1)
@Nullable
private Integer nofileSoftLimit;
@Min(1)
@Nullable
private Integer nofileHardLimit;
@Nullable
private Integer gid;
@Nullable
private Integer uid;
@Nullable
private String user;
@Nullable
private String group;
@Nullable
private String umask;
@Nullable
private Boolean startsAsRoot;
private Boolean registerDefaultExceptionMappers = Boolean.TRUE;
private Boolean detailedJsonProcessingExceptionMapper = Boolean.FALSE;
private Duration shutdownGracePeriod = Duration.seconds(30);
@NotNull
private Set<String> allowedMethods = AllowedMethodsFilter.DEFAULT_ALLOWED_METHODS;
private Optional<String> jerseyRootPath = Optional.empty();
private boolean enableThreadNameFilter = true;
@JsonIgnore
@ValidationMethod(message = "must have a smaller minThreads than maxThreads")
public boolean isThreadPoolSizedCorrectly() {
return minThreads <= maxThreads;
}
@JsonProperty("requestLog")
public synchronized RequestLogFactory getRequestLogFactory() {
if (requestLog == null) {
requestLog = new LogbackAccessRequestLogFactory();
}
return requestLog;
}
@JsonProperty("requestLog")
public synchronized void setRequestLogFactory(RequestLogFactory requestLog) {
this.requestLog = requestLog;
}
@JsonProperty("gzip")
public GzipHandlerFactory getGzipFilterFactory() {
return gzip;
}
@JsonProperty("gzip")
public void setGzipFilterFactory(GzipHandlerFactory gzip) {
this.gzip = gzip;
}
@JsonProperty("serverPush")
public ServerPushFilterFactory getServerPush() {
return serverPush;
}
@JsonProperty("serverPush")
public void setServerPush(ServerPushFilterFactory serverPush) {
this.serverPush = serverPush;
}
@JsonProperty
public int getMaxThreads() {
return maxThreads;
}
@JsonProperty
public void setMaxThreads(int count) {
this.maxThreads = count;
}
@JsonProperty
public int getMinThreads() {
return minThreads;
}
@JsonProperty
public void setMinThreads(int count) {
this.minThreads = count;
}
@JsonProperty
public int getMaxQueuedRequests() {
return maxQueuedRequests;
}
@JsonProperty
public void setMaxQueuedRequests(int maxQueuedRequests) {
this.maxQueuedRequests = maxQueuedRequests;
}
@JsonProperty
public Duration getIdleThreadTimeout() {
return idleThreadTimeout;
}
@JsonProperty
public void setIdleThreadTimeout(Duration idleThreadTimeout) {
this.idleThreadTimeout = idleThreadTimeout;
}
@JsonProperty
@Nullable
public Integer getNofileSoftLimit() {
return nofileSoftLimit;
}
@JsonProperty
public void setNofileSoftLimit(Integer nofileSoftLimit) {
this.nofileSoftLimit = nofileSoftLimit;
}
@JsonProperty
@Nullable
public Integer getNofileHardLimit() {
return nofileHardLimit;
}
@JsonProperty
public void setNofileHardLimit(Integer nofileHardLimit) {
this.nofileHardLimit = nofileHardLimit;
}
@JsonProperty
@Nullable
public Integer getGid() {
return gid;
}
@JsonProperty
public void setGid(Integer gid) {
this.gid = gid;
}
@JsonProperty
@Nullable
public Integer getUid() {
return uid;
}
@JsonProperty
public void setUid(Integer uid) {
this.uid = uid;
}
@JsonProperty
@Nullable
public String getUser() {
return user;
}
@JsonProperty
public void setUser(String user) {
this.user = user;
}
@JsonProperty
@Nullable
public String getGroup() {
return group;
}
@JsonProperty
public void setGroup(String group) {
this.group = group;
}
@JsonProperty
@Nullable
public String getUmask() {
return umask;
}
@JsonProperty
public void setUmask(String umask) {
this.umask = umask;
}
@JsonProperty
@Nullable
public Boolean getStartsAsRoot() {
return startsAsRoot;
}
@JsonProperty
public void setStartsAsRoot(Boolean startsAsRoot) {
this.startsAsRoot = startsAsRoot;
}
public Boolean getRegisterDefaultExceptionMappers() {
return registerDefaultExceptionMappers;
}
public void setRegisterDefaultExceptionMappers(Boolean registerDefaultExceptionMappers) {
this.registerDefaultExceptionMappers = registerDefaultExceptionMappers;
}
public Boolean getDetailedJsonProcessingExceptionMapper() {
return detailedJsonProcessingExceptionMapper;
}
public void setDetailedJsonProcessingExceptionMapper(Boolean detailedJsonProcessingExceptionMapper) {
this.detailedJsonProcessingExceptionMapper = detailedJsonProcessingExceptionMapper;
}
@JsonProperty
public Duration getShutdownGracePeriod() {
return shutdownGracePeriod;
}
@JsonProperty
public void setShutdownGracePeriod(Duration shutdownGracePeriod) {
this.shutdownGracePeriod = shutdownGracePeriod;
}
@JsonProperty
public Set<String> getAllowedMethods() {
return allowedMethods;
}
@JsonProperty
public void setAllowedMethods(Set<String> allowedMethods) {
this.allowedMethods = allowedMethods;
}
@JsonProperty("rootPath")
public Optional<String> getJerseyRootPath() {
return jerseyRootPath;
}
@JsonProperty("rootPath")
public void setJerseyRootPath(String jerseyRootPath) {
this.jerseyRootPath = Optional.ofNullable(jerseyRootPath);
}
@JsonProperty
public boolean getEnableThreadNameFilter() {
return enableThreadNameFilter;
}
@JsonProperty
public void setEnableThreadNameFilter(boolean enableThreadNameFilter) {
this.enableThreadNameFilter = enableThreadNameFilter;
}
protected Handler createAdminServlet(Server server,
MutableServletContextHandler handler,
MetricRegistry metrics,
HealthCheckRegistry healthChecks) {
configureSessionsAndSecurity(handler, server);
handler.setServer(server);
handler.getServletContext().setAttribute(MetricsServlet.METRICS_REGISTRY, metrics);
handler.getServletContext().setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, healthChecks);
handler.addServlet(new NonblockingServletHolder(new AdminServlet()), "/*");
handler.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
.setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, Joiner.on(',').join(allowedMethods));
return handler;
}
private void configureSessionsAndSecurity(MutableServletContextHandler handler, Server server) {
handler.setServer(server);
if (handler.isSecurityEnabled()) {
handler.getSecurityHandler().setServer(server);
}
if (handler.isSessionsEnabled()) {
handler.getSessionHandler().setServer(server);
}
}
protected Handler createAppServlet(Server server,
JerseyEnvironment jersey,
ObjectMapper objectMapper,
Validator validator,
MutableServletContextHandler handler,
@Nullable Servlet jerseyContainer,
MetricRegistry metricRegistry) {
configureSessionsAndSecurity(handler, server);
handler.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
.setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, Joiner.on(',').join(allowedMethods));
if (enableThreadNameFilter) {
handler.addFilter(ThreadNameFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
}
serverPush.addFilter(handler);
if (jerseyContainer != null) {
jerseyRootPath.ifPresent(jersey::setUrlPattern);
jersey.register(new JacksonBinder(objectMapper));
jersey.register(new HibernateValidationFeature(validator));
if (registerDefaultExceptionMappers == null || registerDefaultExceptionMappers) {
jersey.register(new ExceptionMapperBinder(detailedJsonProcessingExceptionMapper));
}
handler.addServlet(new NonblockingServletHolder(jerseyContainer), jersey.getUrlPattern());
}
final InstrumentedHandler instrumented = new InstrumentedHandler(metricRegistry);
instrumented.setServer(server);
instrumented.setHandler(handler);
return instrumented;
}
protected ThreadPool createThreadPool(MetricRegistry metricRegistry) {
final BlockingQueue<Runnable> queue = new BlockingArrayQueue<>(minThreads, maxThreads, maxQueuedRequests);
final InstrumentedQueuedThreadPool threadPool =
new InstrumentedQueuedThreadPool(metricRegistry, maxThreads, minThreads,
(int) idleThreadTimeout.toMilliseconds(), queue);
threadPool.setName("dw");
return threadPool;
}
protected Server buildServer(LifecycleEnvironment lifecycle,
ThreadPool threadPool) {
final Server server = new Server(threadPool);
server.addLifeCycleListener(buildSetUIDListener());
lifecycle.attach(server);
final ErrorHandler errorHandler = new ErrorHandler();
errorHandler.setServer(server);
errorHandler.setShowStacks(false);
server.addBean(errorHandler);
server.setStopAtShutdown(true);
server.setStopTimeout(shutdownGracePeriod.toMilliseconds());
return server;
}
protected SetUIDListener buildSetUIDListener() {
final SetUIDListener listener = new SetUIDListener();
if (startsAsRoot != null) {
listener.setStartServerAsPrivileged(startsAsRoot);
}
if (gid != null) {
listener.setGid(gid);
}
if (uid != null) {
listener.setUid(uid);
}
if (user != null) {
listener.setUsername(user);
}
if (group != null) {
listener.setGroupname(group);
}
if (nofileHardLimit != null || nofileSoftLimit != null) {
final RLimit rlimit = new RLimit();
if (nofileHardLimit != null) {
rlimit.setHard(nofileHardLimit);
}
if (nofileSoftLimit != null) {
rlimit.setSoft(nofileSoftLimit);
}
listener.setRLimitNoFiles(rlimit);
}
if (umask != null) {
listener.setUmaskOctal(umask);
}
return listener;
}
protected Handler addRequestLog(Server server, Handler handler, String name) {
if (getRequestLogFactory().isEnabled()) {
final RequestLogHandler requestLogHandler = new RequestLogHandler();
requestLogHandler.setRequestLog(getRequestLogFactory().build(name));
server.addBean(requestLogHandler.getRequestLog(), true);
requestLogHandler.setHandler(handler);
return requestLogHandler;
}
return handler;
}
protected Handler addStatsHandler(Handler handler) {
final StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(handler);
return statisticsHandler;
}
protected Handler buildGzipHandler(Handler handler) {
return gzip.isEnabled() ? gzip.build(handler) : handler;
}
protected void printBanner(String name) {
try {
final String banner = WINDOWS_NEWLINE.matcher(Resources.toString(Resources.getResource("banner.txt"),
StandardCharsets.UTF_8))
.replaceAll("\n")
.replace("\n", String.format("%n"));
LOGGER.info(String.format("Starting {}%n{}"), name, banner);
} catch (IllegalArgumentException | IOException ignored) {
LOGGER.info("Starting {}", name);
}
}
}