package org.basex;
import static org.basex.core.Text.*;
import static org.basex.http.HTTPText.*;
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
import org.basex.core.*;
import org.basex.http.*;
import org.basex.io.*;
import org.basex.server.*;
import org.basex.server.Log.LogType;
import org.basex.util.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.nio.*;
import org.eclipse.jetty.server.ssl.*;
import org.eclipse.jetty.webapp.*;
import org.eclipse.jetty.xml.*;
/**
* This is the main class for the starting the database HTTP services.
*
* @author BaseX Team 2005-17, BSD License
* @author Christian Gruen
* @author Dirk Kirsten
*/
public final class BaseXHTTP extends Main {
/** Database context. */
private final Context context;
/** HTTP server. */
private final Server jetty;
/** HTTP port. */
private int port;
/** HTTP host. */
private String host;
/** HTTP stop port. */
private int stopPort;
/** Start as daemon. */
private boolean service;
/** Quiet flag. */
private boolean quiet;
/** Stop flag. */
private boolean stop;
/**
* Main method, launching the HTTP services.
* Command-line arguments are listed with the {@code -h} argument.
* @param args command-line arguments
*/
public static void main(final String... args) {
try {
new BaseXHTTP(args);
} catch(final Exception ex) {
Util.errln(ex);
System.exit(1);
}
}
/**
* Constructor.
* @param args command-line arguments
* @throws Exception exception
*/
public BaseXHTTP(final String... args) throws Exception {
super(args);
parseArgs();
// context must be initialized after parsing of arguments
context = HTTPContext.context();
// create jetty instance and set default context to HTTP path
final StaticOptions sopts = context.soptions;
final String webapp = sopts.get(StaticOptions.WEBPATH);
final WebAppContext wac = new WebAppContext(webapp, "/");
jetty = (Server) new XmlConfiguration(initJetty(webapp).inputStream()).configure();
jetty.setHandler(wac);
final Connector[] conns = jetty.getConnectors();
if(conns == null || conns.length == 0)
throw new BaseXException("No Jetty connector defined in " + JETTYCONF + '.');
stopPort = sopts.get(StaticOptions.STOPPORT);
host = sopts.get(StaticOptions.SERVERHOST);
if(port != 0) {
for(final Connector conn : conns) {
if(conn instanceof SelectChannelConnector) {
conn.setPort(port);
break;
}
}
}
// info strings
final String startX = HTTP + ' ' + SRV_STARTED_PORT_X;
final String stopX = HTTP + ' ' + SRV_STOPPED_PORT_X;
if(stop) {
stop();
if(!quiet) for(final Connector conn : conns) Util.outln(stopX, conn.getPort());
// keep message visible for a while
Performance.sleep(1000);
return;
}
// start web server in a new process
final Connector conn1 = conns[0];
if(service) {
start(conn1.getPort(), conn1 instanceof SslSelectChannelConnector, args);
if(!quiet) for(final Connector conn : conns) Util.outln(startX, conn.getPort());
// keep message visible for a while
Performance.sleep(1000);
return;
}
// start web server
if(!quiet) Util.outln(header());
try {
jetty.start();
} catch(final BindException ex) {
Util.debug(ex);
throw new BaseXException(HTTP + ' ' + SRV_RUNNING_X, conn1.getPort());
}
// throw cached exception that did not break the servlet architecture
final IOException ex = HTTPContext.exception();
if(ex != null) throw ex;
// show start message
if(!quiet) {
for(final Connector conn : conns) Util.outln(startX, conn.getPort());
}
// initialize web.xml settings, assign system properties and run database server.
// the call of this function may already have been triggered during the start of jetty
HTTPContext.init(wac.getServletContext());
// start daemon for stopping web server
if(stopPort > 0) new StopServer().start();
// show info when HTTP server is aborted. needs to be called in constructor:
// otherwise, it may only be called if the JVM process is already shut down
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if(!quiet) {
for(final Connector conn : conns) Util.outln(stopX, conn.getPort());
}
final Log log = context.log;
if(log != null) {
for(final Connector conn : conns) {
log.writeServer(LogType.OK, Util.info(stopX, conn.getPort()));
}
}
context.close();
}
});
// log server start at very end (logging flag could have been updated by web.xml)
for(final Connector conn : conns) {
context.log.writeServer(LogType.OK, Util.info(startX, conn.getPort()));
}
}
/**
* Stops the server.
* @throws Exception exception
*/
public void stop() throws Exception {
if(stopPort > 0) stop(host.isEmpty() ? S_LOCALHOST : host, stopPort);
}
/**
* Returns a reference to the Jetty configuration file.
* @param root target root directory
* @return input stream
* @throws IOException I/O exception
*/
private static IOFile initJetty(final String root) throws IOException {
locate(WEBCONF, root);
return locate(JETTYCONF, root);
}
/**
* Locates the specified configuration file.
* @param file file to be copied
* @param root target root directory
* @return reference to created file
* @throws IOException I/O exception
*/
private static IOFile locate(final String file, final String root) throws IOException {
final IOFile trg = new IOFile(root, file);
final boolean create = !trg.exists();
// try to locate file from development branch
final IO in = new IOFile("src/main/webapp", file);
final byte[] data;
if(in.exists()) {
data = in.read();
// check if resource path exists
IOFile res = new IOFile("src/main/resources");
if(res.exists()) {
res = new IOFile(res, file);
// update file in resource path if it has changed
if(!res.exists() || !Token.eq(data, res.read())) {
Util.errln("Updating " + res);
res.parent().md();
res.write(in.read());
}
}
} else if(create) {
// try to locate file from resource path
try(InputStream is = BaseXHTTP.class.getResourceAsStream('/' + file)) {
if(is == null) throw new BaseXException(in + " not found.");
data = new IOStream(is).read();
}
} else {
return trg;
}
if(create) {
// create configuration file
Util.errln("Creating " + trg);
trg.parent().md();
trg.write(data);
}
return trg;
}
@Override
protected void parseArgs() throws IOException {
/* command-line properties will be stored in system properties;
* this way, they will not be overwritten by the settings specified in web.xml. */
final MainParser arg = new MainParser(this);
boolean serve = true;
while(arg.more()) {
if(arg.dash()) {
switch(arg.next()) {
case 'c': // use client mode
Prop.put(StaticOptions.HTTPLOCAL, Boolean.toString(false));
break;
case 'd': // activate debug mode
Prop.put(StaticOptions.DEBUG, Boolean.toString(true));
Prop.debug = true;
break;
case 'D': // hidden flag: daemon mode
serve = false;
break;
case 'h': // parse HTTP port
port = arg.number();
break;
case 'l': // use local mode
Prop.put(StaticOptions.HTTPLOCAL, Boolean.toString(true));
break;
case 'n': // parse host name
final String n = arg.string();
Prop.put(StaticOptions.HOST, n);
Prop.put(StaticOptions.SERVERHOST, n);
break;
case 'p': // parse server port
final int p = arg.number();
Prop.put(StaticOptions.PORT, Integer.toString(p));
Prop.put(StaticOptions.SERVERPORT, Integer.toString(p));
break;
case 'q': // quiet flag (hidden)
quiet = true;
break;
case 's': // parse stop port
Prop.put(StaticOptions.STOPPORT, Integer.toString(arg.number()));
break;
case 'S': // set service flag
service = serve;
break;
case 'U': // specify user name
Prop.put(StaticOptions.USER, arg.string());
break;
case 'z': // suppress logging
Prop.put(StaticOptions.LOG, Boolean.toString(false));
break;
default:
throw arg.usage();
}
} else {
if(!S_STOP.equalsIgnoreCase(arg.string())) throw arg.usage();
stop = true;
}
}
}
// STATIC METHODS ===========================================================
/**
* Starts the HTTP server in a separate process.
* @param port server port
* @param ssl encryption via HTTPS
* @param args command-line arguments
* @throws BaseXException database exception
*/
public static void start(final int port, final boolean ssl, final String... args)
throws BaseXException {
// start server and check if it caused an error message
final String error = Util.error(Util.start(BaseXHTTP.class, args), 2000);
if(error != null) throw new BaseXException(error.trim());
// try to connect to the new server instance
if(!ping(S_LOCALHOST, port, ssl)) throw new BaseXException(CONNECTION_ERROR_X, port);
}
/**
* Stops the server.
* @param host server host
* @param port server port
* @throws IOException I/O exception
*/
public static void stop(final String host, final int port) throws IOException {
// create stop file
final IOFile stopFile = stopFile(BaseXHTTP.class, port);
stopFile.touch();
// try to connect the server
try(Socket s = new Socket(host, port)) {
} catch(final ConnectException ex) {
Util.debug(ex);
stopFile.delete();
throw new IOException(Util.info(CONNECTION_ERROR_X, port));
}
// wait until server was stopped
do Performance.sleep(10); while(stopFile.exists());
}
/**
* Checks if a server is running.
* @param host host
* @param port server port
* @param ssl encryption via HTTPS
* @return boolean success
*/
public static boolean ping(final String host, final int port, final boolean ssl) {
try(InputStream is = new IOUrl((ssl ? "https://" : "http://") + host + ':' + port).
connection().getInputStream()) {
// create connection
return true;
} catch(final FileNotFoundException | SSLHandshakeException ex) {
// if page is not found, server is running
// if SSL handshake failed, server is running, otherwise SSLException
Util.debug(ex);
return true;
} catch(final IOException ex) {
Util.debug(ex);
return false;
}
}
@Override
public String header() {
return Util.info(S_CONSOLE_X, S_HTTP_SERVER);
}
@Override
public String usage() {
return S_HTTPINFO;
}
/** Monitor for stopping the Jetty server. */
private final class StopServer extends Thread {
/** Server socket. */
private final ServerSocket socket;
/** Stop file. */
private final IOFile stopFile;
/**
* Constructor.
* @throws IOException I/O exception
*/
StopServer() throws IOException {
final InetAddress addr = host.isEmpty() ? null : InetAddress.getByName(host);
socket = new ServerSocket();
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(addr, stopPort));
stopFile = stopFile(BaseXHTTP.class, stopPort);
}
@Override
public void run() {
try {
while(true) {
Util.outln(HTTP + ' ' + STOP + ' ' + SRV_STARTED_PORT_X, stopPort);
try(Socket s = socket.accept()) { }
if(stopFile.exists()) {
socket.close();
Util.outln(HTTP + ' ' + STOP + ' ' + SRV_STOPPED_PORT_X, stopPort);
jetty.stop();
HTTPContext.close();
Prop.clear();
if(!stopFile.delete()) {
context.log.writeServer(LogType.ERROR, Util.info(FILE_NOT_DELETED_X, stopFile));
}
break;
}
}
} catch(final Exception ex) {
Util.stack(ex);
}
}
}
}