package io.eguan.rest.container;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* 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.
* #L%
*/
import io.eguan.configuration.MetaConfiguration;
import io.eguan.rest.jaxrs.JaxRsConfiguredApplication;
import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.ShutdownThread;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jersey.spi.container.servlet.ServletContainer;
/**
* JAX-RS application wrapper combining a {@link JettyConfigurationContext configuration} with a given
* {@link WebAppContext} into a runnable server based on a Jetty {@link Server}.
*
* @author oodrive
* @author pwehrle
* @author ebredzinski
*
*/
public final class ServletServer {
private static final Logger LOGGER = LoggerFactory.getLogger(ServletServer.class);
/**
* The possible states of the server.
*
*
*/
public enum State {
/**
* State of failure after {@link ServletServer#start()} or {@link ServletServer#stop()} throws an exception.
*/
FAILED,
/**
* Transitional state between {@link #STOPPED} and (@link #STARTED} or {@link #FAILED}, considered as running
* state.
*/
STARTING,
/**
* State of running after successfully returning from {@link ServletServer#start()}.
*/
STARTED,
/**
* Transitional state between {@link #STARTED} and (@link #STOPPED} or {@link #FAILED}.
*/
STOPPING,
/**
* State following an successful {@link ServletServer#init()} or {@link ServletServer#stop()}.
*/
STOPPED,
/**
* State following construction or after a successful {@link ServletServer#fini()}.
*/
NOT_INITIALIZED,
/**
* Default state provided after successful {@link ServletServer#init()}, if the running state of the server
* cannot be determined.
*/
UNKNOWN_RUNNING_STATE;
}
/**
* The IP address to which the {@link #server} binds.
*
* @see ServerAddressConfigKey
*/
private final InetAddress serverAddress;
/**
* The TCP port to which the {@link #server} binds.
*/
private final int serverPort;
/**
* Whether to include shutdown of the {@link #server} in Jetty's {@link ShutdownThread}.
*
* @see JettyStopAtShutdownConfigKey
*/
private final Boolean stopAtShutdown;
/**
* The {@link JaxRsConfiguredApplication} passed as constructor parameter.
*/
private final JaxRsConfiguredApplication jaxRsApp;
/**
* Lock for all methods depending on the {@link #initialized} state.
*/
private final Object stateLock = new Object();
@GuardedBy("stateLock")
private volatile boolean initialized = false;
@GuardedBy("stateLock")
private Server server;
/**
* The context path for rest
*/
private final String restContextPath;
/**
* The context path for webui
*/
private final String webuiContextPath;
/**
* The war name for webui
*/
private final String webuiWarName;
private static final Handler[] EMPTY_HANDLER_ARRAY = new Handler[0];
/**
* the root path for the REST servlet
*/
private static final String REST_SERVLET_ROOTPATH = "/*";
/**
* The {@link URL} representing the web application's resource base.
*
* A valid {@link URL} instance is least likely to be rejected by Jetty upon initialization.
*
* @see WebAppContext#setResourceBase(String)
*/
private final URL resourceBase;
/**
* Constructs a server instance from a {@link MetaConfiguration} and a given {@link WebAppContext}.
*
* @param configuration
* a non-<code>null</code> {@link MetaConfiguration} initialized with a {@link JettyConfigurationContext}
* @param jaxRsApp
* a non-<code>null</code>, initialized {@link JaxRsConfiguredApplication}
*/
public ServletServer(@Nonnull final MetaConfiguration configuration,
@Nonnull final JaxRsConfiguredApplication jaxRsApp) {
this.jaxRsApp = Objects.requireNonNull(jaxRsApp);
// gets bind address and port from the config to instantiate the server
serverAddress = ServerAddressConfigKey.getInstance().getTypedValue(configuration);
serverPort = ServerPortConfigKey.getInstance().getTypedValue(configuration);
assert (serverAddress != null) && (serverPort > ServerPortConfigKey.MIN_VALUE);
// gets shutdown behavior from the config
stopAtShutdown = JettyStopAtShutdownConfigKey.getInstance().getTypedValue(configuration);
assert (stopAtShutdown != null);
restContextPath = RestContextPathConfigKey.getInstance().getTypedValue(configuration);
assert (restContextPath != null);
resourceBase = RestResourceBaseConfigKey.getInstance().getTypedValue(configuration);
webuiContextPath = WebUiContextPathConfigKey.getInstance().getTypedValue(configuration);
webuiWarName = WebUiWarNameConfigKey.getInstance().getTypedValue(configuration);
}
/**
* Initializes the runtime state of the server.
*
* This method transitions this instance into the {@link #isInitialized() initialized} state, where all runtime
* parameters have been set and calling {@link #start()} won't throw an exception.
*
* @throws IllegalStateException
* if configuration and/or initialization fails
*/
public final void init() throws IllegalStateException {
synchronized (stateLock) {
if (initialized) {
LOGGER.warn("Already initialized");
return;
}
final ArrayList<Handler> handlerList = new ArrayList<Handler>();
// REST
final WebAppContext restWebContext = new WebAppContext();
restWebContext.setResourceBase(resourceBase.toExternalForm());
final ServletContainer jerseyServlet = new ServletContainer(jaxRsApp);
restWebContext.setContextPath(restContextPath);
restWebContext.addServlet(new ServletHolder(jerseyServlet), REST_SERVLET_ROOTPATH);
handlerList.add(restWebContext);
// WEBUI (optional)
if (!webuiWarName.isEmpty()) {
if (Files.exists(new File(webuiWarName).toPath())) {
final WebAppContext webuiWebContext = new WebAppContext();
webuiWebContext.setContextPath(webuiContextPath);
webuiWebContext.setWar(webuiWarName);
handlerList.add(webuiWebContext);
}
else {
LOGGER.error("Webui war: " + webuiWarName + " can not be found.");
}
}
final ContextHandlerCollection contexts = new ContextHandlerCollection();
contexts.setHandlers(handlerList.toArray(EMPTY_HANDLER_ARRAY));
server = new Server(new InetSocketAddress(serverAddress, serverPort));
server.setHandler(contexts);
server.setStopAtShutdown(stopAtShutdown);
initialized = true;
}
}
/**
* Gets the initialization status of this instance.
*
* @return initialization status
*/
public final boolean isInitialized() {
return initialized;
}
/**
* Resets the server to the un-initialized state it's in right after construction.
*
* Returns without change if the server is not yet initialized.
*
* @throws IllegalStateException
* if the server is {@link #isStarted() started}
*/
public final void fini() throws IllegalStateException {
if (isStarted()) {
throw new IllegalStateException("Server is running");
}
synchronized (stateLock) {
if (!initialized) {
LOGGER.warn("Not initialized");
return;
}
server = null;
initialized = false;
}
}
/**
* Starts the server.
*
* @throws IllegalStateException
* if the server is not initalized
* @throws Exception
* if starting the initalized server fails, including a transition to {@link State#FAILED}
*/
public final void start() throws Exception {
synchronized (stateLock) {
if (!initialized) {
throw new IllegalStateException("Not initialized");
}
server.start();
}
}
/**
* Stops the server.
*
* Does nothing if the server is not initialized.
*
* @throws Exception
* if stopping the running server fails, including a transition to {@link State#FAILED}
*/
public final void stop() throws Exception {
synchronized (stateLock) {
if (!initialized) {
return;
}
server.stop();
}
}
/**
* Returns the started status of the server.
*
* @return <code>true</code> if the server is initialized and {@link #getExecutionState()} returns either
* {@link State#STARTING} or {@link State#STARTED}
*/
public final boolean isStarted() {
synchronized (stateLock) {
return initialized && server.isRunning();
}
}
/**
* Returns the execution state as an {@link State} constant.
*
* Once initialized, the running state of the server is returned.
*
* @see State
*
* @return an {@link State}
*/
public final State getExecutionState() {
synchronized (stateLock) {
if (!initialized) {
return State.NOT_INITIALIZED;
}
try {
return State.valueOf(server.getState());
}
catch (final IllegalArgumentException ie) {
return State.UNKNOWN_RUNNING_STATE;
}
}
}
/**
* Blocks until the {@link ServletServer} is {@link State#STOPPED stopped}.
*
* Returns immediately if the server is already stopped.
*
* @throws InterruptedException
* if the thread's execution is abnormally terminated
* @throws IllegalStateException
* if the {@link ServletServer} is not {@link #init() initialized}
*/
public final void join() throws InterruptedException {
// not taking the lock, as blocking here would render the server unstoppable
if (!initialized) {
throw new IllegalStateException("Not initialized");
}
server.join();
}
}