/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2014 The eXist-db Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package org.exist.jetty;
import java.io.IOException;
import java.io.InputStream;
import java.io.LineNumberReader;
import java.io.Reader;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import javax.servlet.Servlet;
import net.jcip.annotations.GuardedBy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.exist.SystemProperties;
import org.exist.start.Main;
import org.exist.storage.BrokerPool;
import org.exist.util.ConfigurationHelper;
import org.exist.util.FileUtils;
import org.exist.util.SingleInstanceConfiguration;
import org.exist.validation.XmlLibraryChecker;
import org.exist.xmldb.DatabaseImpl;
import org.exist.xmldb.ShutdownListener;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Database;
/**
* This class provides a main method to start Jetty with eXist. It registers shutdown
* handlers to cleanly shut down the database and the webserver.
*
* @author wolf
*/
public class JettyStart extends Observable implements LifeCycle.Listener {
public static final String JETTY_HOME_PROP = "jetty.home";
public static final String JETTY_BASE_PROP = "jetty.base";
private static final String JETTY_PROPETIES_FILENAME = "jetty.properties";
private static final Logger logger = LogManager.getLogger(JettyStart.class);
public static void main(final String[] args) {
final JettyStart start = new JettyStart();
start.run(args, null);
}
public final static String SIGNAL_STARTING = "jetty starting";
public final static String SIGNAL_STARTED = "jetty started";
public final static String SIGNAL_ERROR = "error";
private final static int STATUS_STARTING = 0;
private final static int STATUS_STARTED = 1;
private final static int STATUS_STOPPING = 2;
private final static int STATUS_STOPPED = 3;
@GuardedBy("this") private int status = STATUS_STOPPED;
@GuardedBy("this") private Optional<Thread> shutdownHook = Optional.empty();
@GuardedBy("this") private int primaryPort = 8080;
public JettyStart() {
// Additional checks XML libs @@@@
XmlLibraryChecker.check();
}
public synchronized void run() {
final String jettyProperty = Optional.ofNullable(System.getProperty(JETTY_HOME_PROP))
.orElseGet(() -> {
final Optional<Path> home = ConfigurationHelper.getExistHome();
final Path jettyHome = FileUtils.resolve(home, "tools").resolve("jetty");
final String jettyPath = jettyHome.toAbsolutePath().toString();
System.setProperty(JETTY_HOME_PROP, jettyPath);
return jettyPath;
});
final Path standaloneFile = Paths.get(jettyProperty).resolve("etc").resolve(Main.STANDALONE_ENABLED_JETTY_CONFIGS);
run(new String[] { standaloneFile.toAbsolutePath().toString() }, null);
}
public synchronized void run(final String[] args, final Observer observer) {
if (args.length == 0) {
logger.error("No configuration file specified!");
return;
}
final Path jettyConfig = Paths.get(args[0]);
if(Files.notExists(jettyConfig)) {
logger.error("Configuration file: {} does not exist!", jettyConfig.toAbsolutePath().toString());
return;
}
final Map<String, String> configProperties;
try {
configProperties = getConfigProperties(jettyConfig.getParent());
if (observer != null) {
addObserver(observer);
}
logger.info("Running with Java {} [{} ({}) in {}]",
System.getProperty("java.version", "(unknown java.version)"),
System.getProperty("java.vendor", "(unknown java.vendor)"),
System.getProperty("java.vm.name", "(unknown java.vm.name)"),
System.getProperty("java.home", "(unknown java.home)")
);
logger.info("Running as user '{}'", System.getProperty("user.name", "(unknown user.name)"));
logger.info("[eXist Home : {}]", System.getProperty("exist.home", "unknown"));
logger.info("[eXist Version : {}]", SystemProperties.getInstance().getSystemProperty("product-version", "unknown"));
logger.info("[eXist Build : {}]", SystemProperties.getInstance().getSystemProperty("product-build", "unknown"));
logger.info("[Git commit : {}]", SystemProperties.getInstance().getSystemProperty("git-commit", "unknown"));
logger.info("[Operating System : {} {} {}]", System.getProperty("os.name"), System.getProperty("os.version"), System.getProperty("os.arch"));
logger.info("[log4j.configurationFile : {}]", System.getProperty("log4j.configurationFile"));
logger.info("[{} : {}]", JETTY_HOME_PROP, configProperties.get(JETTY_HOME_PROP));
logger.info("[{} : {}]", JETTY_BASE_PROP, configProperties.get(JETTY_BASE_PROP));
logger.info("[jetty configuration : {}]", jettyConfig.toAbsolutePath().toString());
// configure the database instance
SingleInstanceConfiguration config;
if (args.length == 2) {
config = new SingleInstanceConfiguration(args[1]);
} else {
config = new SingleInstanceConfiguration();
}
logger.info("Configuring eXist from {}",
config.getConfigFilePath()
.map(Path::normalize).map(Path::toAbsolutePath).map(Path::toString)
.orElse("<UNKNOWN>"));
BrokerPool.configure(1, 5, config, Optional.ofNullable(observer));
// register the XMLDB driver
final Database xmldb = new DatabaseImpl();
xmldb.setProperty("create-database", "false");
DatabaseManager.registerDatabase(xmldb);
} catch (final Exception e) {
logger.error("configuration error: " + e.getMessage(), e);
e.printStackTrace();
return;
}
try {
// load jetty configurations
final List<Path> configFiles = getEnabledConfigFiles(jettyConfig);
final List<Object> configuredObjects = new ArrayList<>();
XmlConfiguration last = null;
for(final Path confFile : configFiles) {
logger.info("[loading jetty configuration : {}]", confFile.toString());
try(final InputStream is = Files.newInputStream(confFile)) {
final XmlConfiguration configuration = new XmlConfiguration(is);
if (last != null) {
configuration.getIdMap().putAll(last.getIdMap());
}
configuration.getProperties().putAll(configProperties);
configuredObjects.add(configuration.configure());
last = configuration;
}
}
// start Jetty
final Optional<Server> maybeServer = startJetty(configuredObjects);
if(!maybeServer.isPresent()) {
logger.error("Unable to find a server to start in jetty configurations");
throw new IllegalStateException();
}
final Server server = maybeServer.get();
final Connector[] connectors = server.getConnectors();
// Construct description of all ports opened.
final StringBuilder allPorts = new StringBuilder();
if (connectors.length > 1) {
// plural s
allPorts.append("s");
}
boolean establishedPrimaryPort = false;
for(final Connector connector : connectors) {
if(connector instanceof NetworkConnector) {
final NetworkConnector networkConnector = (NetworkConnector)connector;
if(!establishedPrimaryPort) {
this.primaryPort = networkConnector.getLocalPort();
establishedPrimaryPort = true;
}
allPorts.append(" ");
allPorts.append(networkConnector.getLocalPort());
}
}
//TODO: use pluggable interface
Class<?> openid = null;
try {
openid = Class.forName("org.exist.security.realm.openid.AuthenticatorOpenIdServlet");
} catch (final NoClassDefFoundError | ClassNotFoundException e) {
logger.warn("Could not find OpenID extension. OpenID will be disabled!");
}
Class<?> oauth = null;
try {
oauth = Class.forName("org.exist.security.realm.oauth.OAuthServlet");
} catch (final NoClassDefFoundError | ClassNotFoundException e) {
logger.warn("Could not find OAuthServlet extension. OAuth will be disabled!");
}
Class<?> iprange = null;
try {
iprange = Class.forName("org.exist.security.realm.iprange.IPRangeServlet");
} catch (final NoClassDefFoundError | ClassNotFoundException e) {
logger.warn("Could not find IPRangeServlet extension. IPRange will be disabled!");
}
//*************************************************************
final List<URI> serverUris = getSeverURIs(server);
if(!serverUris.isEmpty()) {
this.primaryPort = serverUris.get(0).getPort();
}
logger.info("-----------------------------------------------------");
logger.info("Server has started, listening on:");
for(final URI serverUri : serverUris) {
logger.info("\t{}", serverUri.resolve("/"));
}
logger.info("Configured contexts:");
final LinkedHashSet<Handler> handlers = getAllHandlers(server.getHandler());
for (final Handler handler: handlers) {
if (handler instanceof ContextHandler) {
final ContextHandler contextHandler = (ContextHandler) handler;
logger.info("\t{}", contextHandler.getContextPath());
}
//TODO(AR) openid and oauth servlet configs should be moved to the exist-webapp-context or into $EXIST_HOME/webapp/WEB-INF/web.xml
if (openid != null) {
if (handler instanceof ServletContextHandler) {
final ServletContextHandler contextHandler = (ServletContextHandler) handler;
contextHandler.addServlet(new ServletHolder((Class<? extends Servlet>) openid), "/openid");
String suffix;
if (contextHandler.getContextPath().endsWith("/")) {
suffix = "openid";
} else {
suffix = "/openid";
}
logger.info("\t{}", contextHandler.getContextPath() + suffix);
}
}
if (oauth != null) {
if (handler instanceof ServletContextHandler) {
final ServletContextHandler contextHandler = (ServletContextHandler) handler;
contextHandler.addServlet(new ServletHolder((Class<? extends Servlet>) oauth), "/oauth/*");
String suffix;
if (contextHandler.getContextPath().endsWith("/")) {
suffix = "oauth";
} else {
suffix = "/oauth";
}
logger.info("\t{}", contextHandler.getContextPath() + suffix);
}
}
if (iprange != null) {
if (handler instanceof ServletContextHandler) {
final ServletContextHandler contextHandler = (ServletContextHandler) handler;
contextHandler.addServlet(new ServletHolder((Class<? extends Servlet>) iprange), "/iprange");
String suffix;
if (contextHandler.getContextPath().endsWith("/")) {
suffix = "iprange";
} else {
suffix = "/iprange";
}
logger.info("'" + contextHandler.getContextPath() + suffix + "'");
}
}
//*************************************************************
}
logger.info("-----------------------------------------------------");
setChanged();
notifyObservers(SIGNAL_STARTED);
} catch (final MultiException e) {
// Mute the BindExceptions
boolean hasBindException = false;
for (final Throwable t : e.getThrowables()) {
if (t instanceof java.net.BindException) {
hasBindException = true;
logger.error("----------------------------------------------------------");
logger.error("ERROR: Could not bind to port because {}", t.getMessage());
logger.error(t.toString());
logger.error("----------------------------------------------------------");
}
}
// If it is another error, print stacktrace
if (!hasBindException) {
e.printStackTrace();
}
setChanged();
notifyObservers(SIGNAL_ERROR);
} catch (final SocketException e) {
logger.error("----------------------------------------------------------");
logger.error("ERROR: Could not bind to port because {}", e.getMessage());
logger.error(e.toString());
logger.error("----------------------------------------------------------");
setChanged();
notifyObservers(SIGNAL_ERROR);
} catch (final Exception e) {
e.printStackTrace();
setChanged();
notifyObservers(SIGNAL_ERROR);
}
}
private LinkedHashSet<Handler> getAllHandlers(final Handler handler) {
if(handler instanceof HandlerWrapper) {
final HandlerWrapper handlerWrapper = (HandlerWrapper) handler;
final LinkedHashSet<Handler> handlers = new LinkedHashSet<>();
handlers.add(handlerWrapper);
if(handlerWrapper.getHandler() != null) {
handlers.addAll(getAllHandlers(handlerWrapper.getHandler()));
}
return handlers;
} else if(handler instanceof HandlerContainer) {
final HandlerContainer handlerContainer = (HandlerContainer) handler;
final LinkedHashSet<Handler> handlers = new LinkedHashSet<>();
handlers.add(handler);
for(final Handler childHandler : handlerContainer.getChildHandlers()) {
handlers.addAll(getAllHandlers(childHandler));
}
return handlers;
} else {
//assuming just Handler
final LinkedHashSet<Handler> handlers = new LinkedHashSet<>();
handlers.add(handler);
return handlers;
}
}
/**
* See {@link Server#getURI()}
*/
private List<URI> getSeverURIs(final Server server) {
final ContextHandler context = server.getChildHandlerByClass(ContextHandler.class);
return Arrays.stream(server.getConnectors())
.filter(connector -> connector instanceof NetworkConnector)
.map(connector -> (NetworkConnector)connector)
.map(networkConnector -> getURI(networkConnector, context))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* See {@link Server#getURI()}
*/
private URI getURI(final NetworkConnector networkConnector, final ContextHandler context) {
try {
final String protocol = networkConnector.getDefaultConnectionFactory().getProtocol();
final String scheme;
if (protocol.startsWith("SSL-") || protocol.equals("SSL")) {
scheme = "https";
} else {
scheme = "http";
}
String host = null;
if (context != null && context.getVirtualHosts() != null && context.getVirtualHosts().length > 0) {
host = context.getVirtualHosts()[0];
} else {
host = networkConnector.getHost();
}
if (host == null) {
host = InetAddress.getLocalHost().getHostAddress();
}
String path = context == null ? null : context.getContextPath();
if (path == null) {
path = "/";
}
return new URI(scheme, null, host, networkConnector.getLocalPort(), path, null, null);
} catch(final UnknownHostException | URISyntaxException e) {
logger.warn(e);
return null;
}
}
private Optional<Server> startJetty(final List<Object> configuredObjects) throws Exception {
// For all objects created by XmlConfigurations, start them if they are lifecycles.
Optional<Server> server = Optional.empty();
for (final Object configuredObject : configuredObjects) {
if(configuredObject instanceof Server) {
final Server _server = (Server)configuredObject;
//skip this server if we have already started it
if(server.map(configuredServer -> configuredServer == _server).orElse(false)) {
continue;
}
//setup server shutdown
_server.addLifeCycleListener(this);
BrokerPool.getInstance().registerShutdownListener(new ShutdownListenerImpl(_server));
// register a shutdown hook for the server
final BrokerPoolAndJettyShutdownHook brokerPoolAndJettyShutdownHook =
new BrokerPoolAndJettyShutdownHook(_server);
Runtime.getRuntime().addShutdownHook(brokerPoolAndJettyShutdownHook);
this.shutdownHook = Optional.of(brokerPoolAndJettyShutdownHook);
server = Optional.of(_server);
}
if (configuredObject instanceof LifeCycle) {
final LifeCycle lc = (LifeCycle)configuredObject;
if (!lc.isRunning()) {
logger.info("[Starting jetty component : {}]", lc.getClass().getName());
lc.start();
}
}
}
return server;
}
private Map<String, String> getConfigProperties(final Path configDir) throws IOException {
final Map<String, String> configProperties = new HashMap<>();
//load jetty.properties file
final Path propertiesFile = configDir.resolve(JETTY_PROPETIES_FILENAME);
if(Files.exists(propertiesFile)) {
final Properties jettyProperties = new Properties();
try(final Reader reader = Files.newBufferedReader(propertiesFile)) {
jettyProperties.load(reader);
logger.info("Loaded jetty.properties from: {}", propertiesFile.toAbsolutePath().toString());
for(final Map.Entry<Object, Object> property : jettyProperties.entrySet()) {
configProperties.put(property.getKey().toString(), property.getValue().toString());
}
}
}
// set or override jetty.home and jetty.base with System properties
configProperties.put(JETTY_HOME_PROP, System.getProperty(JETTY_HOME_PROP));
configProperties.put(JETTY_BASE_PROP, System.getProperty(JETTY_BASE_PROP, System.getProperty(JETTY_HOME_PROP)));
return configProperties;
}
private List<Path> getEnabledConfigFiles(final Path enabledJettyConfigs) throws IOException {
if(Files.notExists(enabledJettyConfigs)) {
throw new IOException("Cannot find config enabler: " + enabledJettyConfigs.toString());
} else {
final List<Path> configFiles = new ArrayList<>();
try (final LineNumberReader reader = new LineNumberReader(Files.newBufferedReader(enabledJettyConfigs))) {
String line = null;
while ((line = reader.readLine()) != null) {
final String tl = line.trim();
if (tl.isEmpty() || tl.charAt(0) == '#') {
continue;
} else {
final Path configFile = enabledJettyConfigs.getParent().resolve(tl);
if (Files.notExists(configFile)) {
throw new IOException("Cannot find enabled config: " + configFile.toString());
} else {
configFiles.add(configFile);
}
}
}
}
return configFiles;
}
}
public synchronized void shutdown() {
shutdownHook.ifPresent(Runtime.getRuntime()::removeShutdownHook);
BrokerPool.stopAll(false);
while (status != STATUS_STOPPED) {
try {
wait();
} catch (final InterruptedException e) {
// ignore
}
}
}
/**
* This class gets called after the database received a shutdown request.
*
* @author wolf
*/
private static class ShutdownListenerImpl implements ShutdownListener {
private final Server server;
ShutdownListenerImpl(final Server server) {
this.server = server;
}
@Override
public void shutdown(final String dbname, final int remainingInstances) {
logger.info("Database shutdown: stopping server in 1sec ...");
if (remainingInstances == 0) {
// give the webserver a 1s chance to complete open requests
final Timer timer = new Timer("jetty shutdown schedule", true);
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
// stop the server
server.stop();
server.join();
} catch (final Exception e) {
e.printStackTrace();
}
}
}, 1000); // timer.schedule
}
}
}
private static class BrokerPoolAndJettyShutdownHook extends Thread {
private final Server server;
BrokerPoolAndJettyShutdownHook(final Server server) {
super("JettyStart-ShutdownHook");
this.server = server;
}
@Override
public void run() {
BrokerPool.stopAll(true);
if (server.isStopping() || server.isStopped()) {
return;
}
try {
server.stop();
} catch (final Exception e) {
e.printStackTrace();
}
}
}
public synchronized boolean isStarted() {
if (status == STATUS_STARTED || status == STATUS_STARTING) {
return true;
}
if (status == STATUS_STOPPED) {
return false;
}
while (status != STATUS_STOPPED) {
try {
wait();
} catch (final InterruptedException e) {
}
}
return false;
}
@Override
public synchronized void lifeCycleStarting(final LifeCycle lifeCycle) {
logger.info("Jetty server starting...");
setChanged();
notifyObservers(SIGNAL_STARTING);
status = STATUS_STARTING;
notifyAll();
}
@Override
public synchronized void lifeCycleStarted(final LifeCycle lifeCycle) {
logger.info("Jetty server started.");
setChanged();
notifyObservers(SIGNAL_STARTED);
status = STATUS_STARTED;
notifyAll();
}
@Override
public void lifeCycleFailure(final LifeCycle lifeCycle, final Throwable throwable) {
}
@Override
public synchronized void lifeCycleStopping(final LifeCycle lifeCycle) {
logger.info("Jetty server stopping...");
status = STATUS_STOPPING;
notifyAll();
}
@Override
public synchronized void lifeCycleStopped(final LifeCycle lifeCycle) {
logger.info("Jetty server stopped");
status = STATUS_STOPPED;
notifyAll();
}
public synchronized int getPrimaryPort() {
return primaryPort;
}
}