/* * eXist Open Source Native XML Database * Copyright (C) 2001-04 The eXist 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., 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id$ */ package org.exist; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.*; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.avalon.excalibur.cli.CLArgsParser; import org.apache.avalon.excalibur.cli.CLOption; import org.apache.avalon.excalibur.cli.CLOptionDescriptor; import org.apache.avalon.excalibur.cli.CLUtil; import org.apache.log4j.Logger; import org.exist.memtree.SAXAdapter; import org.exist.storage.BrokerPool; import org.exist.util.Configuration; import org.exist.util.ConfigurationHelper; import org.exist.xmldb.ShutdownListener; import org.mortbay.http.HttpContext; import org.mortbay.http.HttpListener; import org.mortbay.http.HttpServer; import org.mortbay.http.SocketListener; import org.mortbay.http.SslListener; import org.mortbay.http.handler.ForwardHandler; import org.mortbay.http.handler.NotFoundHandler; import org.mortbay.jetty.servlet.ServletHolder; import org.mortbay.jetty.servlet.WebApplicationHandler; import org.mortbay.util.MultiException; import org.mortbay.util.ThreadedServer; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xmldb.api.DatabaseManager; import org.xmldb.api.base.Database; /** * Starts eXist in standalone server mode. In this mode, only the XMLRPC, REST and WebDAV * interfaces are provided. By default, the XMLRPC interface runs on port 8081. A minimal Jetty * webserver configuration is used for the REST and WebDAV interfaces. The REST interface * is accessible on <a href="http://localhost:8088">http://localhost:8088</a> by default. The * WebDAV server uses the URL <a href="http://localhost:8088/webdav/db"> * http://localhost:8088/webdav/db</a> for the database root collection. * * @author wolf */ public class StandaloneServer { public interface ServletBootstrap { void bootstrap(Properties properties, WebApplicationHandler handler); } private final static Logger LOG = Logger.getLogger(StandaloneServer.class); // command-line options private final static int HELP_OPT = 'h'; private final static int DEBUG_OPT = 'd'; private final static int HTTP_PORT_OPT = 'p'; private final static int THREADS_OPT = 't'; private final static CLOptionDescriptor OPTIONS[] = new CLOptionDescriptor[] { new CLOptionDescriptor( "help", CLOptionDescriptor.ARGUMENT_DISALLOWED, HELP_OPT, "print help on command line options and exit." ), new CLOptionDescriptor( "debug", CLOptionDescriptor.ARGUMENT_DISALLOWED, DEBUG_OPT, "debug XMLRPC calls." ), new CLOptionDescriptor( "http-port", CLOptionDescriptor.ARGUMENT_REQUIRED, HTTP_PORT_OPT, "set HTTP port." ), new CLOptionDescriptor( "threads", CLOptionDescriptor.ARGUMENT_REQUIRED, THREADS_OPT, "set max. number of parallel threads allowed by the db." ) }; private final static String DEFAULT_HTTP_LISTENER_PORT = "8088"; private static Properties DEFAULT_PROPERTIES = new Properties(); static { DEFAULT_PROPERTIES.setProperty("webdav.enabled", "yes"); DEFAULT_PROPERTIES.setProperty("rest.enabled", "yes"); DEFAULT_PROPERTIES.setProperty("xmlrpc.enabled", "yes"); DEFAULT_PROPERTIES.setProperty("webdav.authentication", "basic"); DEFAULT_PROPERTIES.setProperty("rest.form.encoding", "UTF-8"); DEFAULT_PROPERTIES.setProperty("rest.container.encoding", "UTF-8"); DEFAULT_PROPERTIES.setProperty("rest.param.dynamic-content-type", "no"); } private HttpServer httpServer; private Map forwarding = new HashMap(); private Map listeners = new HashMap(); private Map filters = new HashMap(); public StandaloneServer() { } public void run(String[] args) throws Exception { run(args, null); } public void run(String[] args, Observer observer) throws Exception { printNotice(); //set default properties Properties props = new Properties(DEFAULT_PROPERTIES); //set default listener Properties defaultListener = new Properties(); defaultListener.setProperty("port", DEFAULT_HTTP_LISTENER_PORT); listeners.put("http", defaultListener); //read the configuration file List servlets = configure(props); CLArgsParser optParser = new CLArgsParser( args, OPTIONS ); if(optParser.getErrorString() != null) { LOG.error( "ERROR: " + optParser.getErrorString()); return; } List opt = optParser.getArguments(); int size = opt.size(); CLOption option; int threads = 5; for(int i = 0; i < size; i++) { option = (CLOption)opt.get(i); switch(option.getId()) { case HELP_OPT : printHelp(); return; case DEBUG_OPT : break; case HTTP_PORT_OPT : Properties httpListener = (Properties)listeners.get("http"); httpListener.put("port", option.getArgument()); listeners.put("http", httpListener); break; case THREADS_OPT : try { threads = Integer.parseInt( option.getArgument() ); } catch( NumberFormatException e ) { LOG.error("option -t requires a numeric argument", e); return; } break; } } LOG.info( "Loading configuration ..."); Configuration config = new Configuration("conf.xml"); if (observer != null) BrokerPool.registerStatusObserver(observer); BrokerPool.configure( 1, threads, config ); BrokerPool.getInstance().registerShutdownListener(new ShutdownListenerImpl()); initXMLDB(); startHttpServer(servlets, props); LOG.info(""); LOG.info("Server launched ..."); LOG.info("Installed services:"); LOG.info("-----------------------------------------------"); Set listenerProtocols = listeners.keySet(); for(int i = 0 ; i < servlets.size() ; i++) { String name = (String) servlets.get(i); if(props.getProperty(name + ".enabled").equalsIgnoreCase("yes")) { for(Iterator itProtocol = listenerProtocols.iterator(); itProtocol.hasNext();) { String listenerProtocol = (String)itProtocol.next(); Properties listenerProperties = (Properties)listeners.get(listenerProtocol); String host = listenerProperties.getProperty("host", "localhost"); String port = listenerProperties.getProperty("port"); LOG.info(name + ":\t" + host + ":" + port + props.getProperty(name+".context")); } } } } public boolean isStarted() { if (httpServer == null) return false; return httpServer.isStarted(); } /** * */ private void initXMLDB() throws Exception { Class clazz = Class.forName("org.exist.xmldb.DatabaseImpl"); Database database = (Database) clazz.newInstance(); database.setProperty("create-database", "true"); DatabaseManager.registerDatabase(database); } /** * Configures a minimal Jetty webserver (no webapplication support, * no file system access) and registers the WebDAV and REST servlets. * * @throws UnknownHostException * @throws IllegalArgumentException * @throws MultiException */ private void startHttpServer(List servlets, Properties props) throws Exception { httpServer = new HttpServer(); //setup listeners Set listenerProtocols = listeners.keySet(); for(Iterator itProtocol = listenerProtocols.iterator(); itProtocol.hasNext();) { String listenerProtocol = (String)itProtocol.next(); Properties listenerProps = (Properties)listeners.get(listenerProtocol); HttpListener listener = null; /** currently support http and https listeners */ if(listenerProtocol.equals("http")) { listener = new SocketListener(); } else if(listenerProtocol.equals("https")) { listener = new SslListener(); Properties params = (Properties)listenerProps.get("params"); //set the keystore if specified if(params.containsKey("keystore")) { String keystore = params.getProperty("keystore"); ((SslListener)listener).setKeystore(keystore); } } if(listener != null) { //configure lisetener listener.setHost(listenerProps.getProperty("host")); String port = (String)listenerProps.get("port"); listener.setPort(Integer.parseInt(port)); String address = (String)listenerProps.get("address"); if(address != null) { InetAddress iaddress = InetAddress.getByName(address); ((ThreadedServer)listener).setInetAddress(iaddress); } ((ThreadedServer)listener).setMinThreads(5); ((ThreadedServer)listener).setMaxThreads(50); httpServer.addListener(listener); } } HttpContext context = new HttpContext(); context.setContextPath("/"); // Setting up resourceBase, if it is possible // This one is needed by many Servlets which depend // on a not null context.getResourceBase() value File eXistHome=ConfigurationHelper.getExistHome(); if(eXistHome!=null) context.setResourceBase(eXistHome.getAbsolutePath()); WebApplicationHandler webappHandler = new WebApplicationHandler(); // TODO: this should be read from a configuration file Map bootstrappers = new HashMap(); bootstrappers.put("rest", new ServletBootstrap() { public void bootstrap(Properties props, WebApplicationHandler webappHandler) { String path = props.getProperty("rest.context", "/*"); ServletHolder restServlet = webappHandler.addServlet("EXistServlet", path, "org.exist.http.servlets.EXistServlet"); restServlet.setInitParameter("form-encoding", props.getProperty("rest.param.form-encoding")); restServlet.setInitParameter("container-encoding", props.getProperty("rest.param.container-encoding")); restServlet.setInitParameter("dynamic-content-type", props.getProperty("rest.param.dynamic-content-type")); String value = props.getProperty("rest.param.use-default-user"); if (value!=null) { restServlet.setInitParameter("use-default-user", value); } value = props.getProperty("rest.param.default-user-username"); if (value!=null) { restServlet.setInitParameter("user", value); } value = props.getProperty("rest.param.default-user-password"); if (value!=null) { restServlet.setInitParameter("password", value); } } }); bootstrappers.put("webdav", new ServletBootstrap() { public void bootstrap(Properties props, WebApplicationHandler webappHandler) { String path = props.getProperty("webdav.context", "/webdav/*"); ServletHolder davServlet = webappHandler.addServlet("WebDAV", path, "org.exist.http.servlets.WebDAVServlet"); davServlet.setInitParameter("authentication", props.getProperty("webdav.param.authentication")); } }); bootstrappers.put("xmlrpc", new ServletBootstrap() { public void bootstrap(Properties props, WebApplicationHandler webappHandler) { String path = props.getProperty("xmlrpc.context", "/xmlrpc/*"); webappHandler.addServlet("RpcServlet", path, "org.exist.xmlrpc.RpcServlet"); } }); for (int i = 0 ; i < servlets.size() ; i++) { String name = (String) servlets.get(i); ServletBootstrap bootstrapper = (ServletBootstrap)bootstrappers.get(name); if (bootstrapper!=null) { bootstrapper.bootstrap(props, webappHandler); } else { String path = props.getProperty(name+".context", "/"+name+"/*"); String sname = props.getProperty(name+".name", name); ServletHolder servlet = webappHandler.addServlet(sname, path, props.getProperty(name+".class")); String paramPrefix = name+".param."; for (Enumeration pnames = props.propertyNames(); pnames.hasMoreElements(); ) { String pname = (String)pnames.nextElement(); if (pname.startsWith(paramPrefix)) { String theName = pname.substring(paramPrefix.length()); servlet.setInitParameter(theName,props.getProperty(pname)); } } } } //setup filters Set filterClasses = filters.keySet(); for(Iterator itFilterClass = filterClasses.iterator(); itFilterClass.hasNext();) { String filterClass = (String)itFilterClass.next(); Properties filterProps = (Properties)filters.get(filterClass); org.mortbay.jetty.servlet.FilterHolder filterHolder = webappHandler.defineFilter(filterClass, filterClass); //TODO: putAll may be wrong??? check this filterHolder.putAll((Properties)filterProps.get("params")); //TODO: Dispatcher.__DEFAULY may be wrong??? check this webappHandler.addFilterPathMapping(filterProps.getProperty("path"), filterClass, org.mortbay.jetty.servlet.Dispatcher.__DEFAULT); } if (forwarding.size() > 0) { ForwardHandler forward = new ForwardHandler(); //forward.setHandleQueries(true); //TODO needed if you wish to pass querystring parameters - should maybe be a server.xml option? for (Iterator i = forwarding.keySet().iterator(); i.hasNext(); ) { String path = (String) i.next(); String destination = (String) forwarding.get(path); if (path.length() == 0) forward.setRootForward(destination); else forward.addForward(path, destination); } context.addHandler(forward); } context.addHandler(webappHandler); context.addHandler(new NotFoundHandler()); httpServer.addContext(context); httpServer.start(); } private static void printHelp() { LOG.info("Usage: java " + StandaloneServer.class.getName() + " [options]"); LOG.info(CLUtil.describeOptions(OPTIONS).toString()); } public static void printNotice() { LOG.info("eXist version 1.4, Copyright (C) 2001-2009 The eXist Project"); LOG.info("eXist comes with ABSOLUTELY NO WARRANTY."); LOG.info("This is free software, and you are welcome to " + "redistribute it\nunder certain conditions; " + "for details read the license file.\n"); } public void shutdown() { BrokerPool.stopAll(false); } private List configure(Properties properties) throws ParserConfigurationException, SAXException, IOException { // try to read configuration from file. Guess the location if // necessary InputStream is = null; String file = System.getProperty("server.xml", "server.xml"); File f = ConfigurationHelper.lookup(file); if (!f.canRead()) { is = StandaloneServer.class.getClassLoader().getResourceAsStream("org/exist/server.xml"); if (is == null) throw new IOException("Server configuration not found!"); LOG.info("Reading server configuration from exist.jar"); } else { LOG.info("Reading server configuration from: " + f.getAbsolutePath()); is = new FileInputStream(f); } // initialize xml parser SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setNamespaceAware(true); factory.setValidating(false); InputSource src = new InputSource(is); SAXParser parser = factory.newSAXParser(); XMLReader reader = parser.getXMLReader(); SAXAdapter adapter = new SAXAdapter(); reader.setContentHandler(adapter); reader.parse(src); Document doc = adapter.getDocument(); Element root = doc.getDocumentElement(); if (!root.getLocalName().equals("server")) { LOG.warn("Configuration should have a root element <server>"); return new ArrayList(); } List configurations = new ArrayList(); NodeList cl = root.getChildNodes(); for (int i = 0; i < cl.getLength(); i++) { Node node = cl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) node; String name = elem.getLocalName(); if ("forwarding".equals(name)) { configureForwards(elem); } else if ("listener".equals(name)) { configureListener(elem); } else if ("filter".equals(name)) { configureFilter(elem); } else if ("servlet".equals(name)) { String className = elem.getAttribute("class"); configurations.add(className); parseDefaultAttrs(properties, elem, className); properties.putAll(parseParams(elem, className)); } else { configurations.add(name); parseDefaultAttrs(properties, elem, name); properties.putAll(parseParams(elem, name)); } } } return configurations; } private void configureListener(Element listener) { NamedNodeMap listenerAttrs = listener.getAttributes(); Node listenerProtocol = listenerAttrs.getNamedItem("protocol"); Node listenerPort = listenerAttrs.getNamedItem("port"); Node listenerHost = listenerAttrs.getNamedItem("host"); Node listenerAddress = listenerAttrs.getNamedItem("address"); if(listenerProtocol != null && listenerPort != null) { Properties listenerProps = new Properties(); listenerProps.put("port", listenerPort.getNodeValue()); if(listenerHost != null) listenerProps.put("host", listenerHost.getNodeValue()); if(listenerAddress != null) listenerProps.put("address", listenerAddress.getNodeValue()); listenerProps.put("params", parseParams(listener, null)); listeners.put(listenerProtocol.getNodeValue().toLowerCase(), listenerProps); } } private void configureFilter(Element filter) { NamedNodeMap filterAttrs = filter.getAttributes(); Node filterEnabled = filterAttrs.getNamedItem("enabled"); Node filterPath = filterAttrs.getNamedItem("path"); Node filterClass = filterAttrs.getNamedItem("class"); if(filterEnabled != null && filterPath != null && filterClass != null) { if(filterEnabled.getNodeValue().equals("yes")) { Properties filterProps = new Properties(); filterProps.put("path", filterPath.getNodeValue()); filterProps.put("params", parseParams(filter, null)); filters.put(filterClass.getNodeValue(), filterProps); } } } private Properties parseParams(Element root, String prefix) { Properties paramProperties = new Properties(); if(prefix != null) prefix += ".param."; else prefix = ""; NodeList cl = root.getChildNodes(); for (int i = 0; i < cl.getLength(); i++) { Node node = cl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE && "param".equals(node.getLocalName())) { Element elem = (Element) node; String name = elem.getAttribute("name"); String value = elem.getAttribute("value"); if (name != null && name.length() > 0) paramProperties.setProperty(prefix + name, value); } } return paramProperties; } private void configureForwards(Element root) { NodeList cl = root.getChildNodes(); for (int i = 0; i < cl.getLength(); i++) { Node node = cl.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) node; String name = elem.getLocalName(); if ("root".equals(name)) { String dest = elem.getAttribute("destination"); forwarding.put("", dest); } else if ("forward".equals(name)) { String path = elem.getAttribute("path"); String dest = elem.getAttribute("destination"); forwarding.put(path, dest); } } } } /** * @param properties * @param elem */ private void parseDefaultAttrs(Properties properties, Element elem, String prefix) { String [] names = { "enabled", "name", "context", "class" }; for (int i=0; i<names.length; i++) { String attr = elem.getAttribute(names[i]); if (attr != null && attr.length() > 0) { properties.setProperty(prefix + '.' + names[i], attr); } } } class ShutdownListenerImpl implements ShutdownListener { public void shutdown(String dbname, int remainingInstances) { if(remainingInstances == 0) { // give the server a 1s chance to complete pending requests Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { LOG.info("killing threads ..."); try { httpServer.stop(); } catch (InterruptedException e) { e.printStackTrace(); } System.exit(0); } }, 1000); } } } public static void main(String[] args) { StandaloneServer server = new StandaloneServer(); try { server.run(args, null); } catch (Exception e) { LOG.error("An exception occurred while launching the server: " + e.getMessage(), e); } } }