/*
* Copyright 2008 Fedora Commons, Inc.
*
* 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.
*/
package org.mulgara.server;
import static org.eclipse.jetty.servlet.ServletContextHandler.SESSIONS;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Servlet;
import org.apache.log4j.Logger;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HandlerContainer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.nio.BlockingChannelConnector;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.MultiException;
import org.mulgara.config.JettyConnector;
import org.mulgara.config.MulgaraConfig;
import org.mulgara.util.JettyLogger;
import org.mulgara.util.Reflect;
import org.mulgara.util.TempDir;
import org.mulgara.util.functional.Fn1E;
import org.mulgara.util.functional.Pair;
import org.xml.sax.SAXException;
/**
* Manages all the HTTP services provided by a Mulgara server.
*
* @created Sep 5, 2008
* @author Paula Gearon
* @copyright © 2008 <a href="http://www.topazproject.org/">The Topaz Project</a>
*/
public class HttpServicesImpl implements HttpServices {
/** A virtual typedef for a context starter. */
private interface ContextStarter extends Fn1E<Server,Pair<String,String>,IOException> { }
/** A virtual typedef for a service path. */
private class Service extends Pair<String,String> { Service(String f, String s) { super(f,s); } }
/** The logging category to log to. */
protected static final Logger logger = Logger.getLogger(HttpServicesImpl.class.getName());
/** The web application file path. */
private final static String WEBAPP_PATH = "webapps";
/** The Web Services web application file. */
private final static String WEBSERVICES_WEBAPP = "webservices.war";
/** The Web Services path. */
private final static String WEBSERVICES_PATH = "webservices";
/** The Web Query path. */
private final static String WEBQUERY_PATH = "webui";
/** The Web Tutorial path. */
private final static String WEBTUTORIAL_PATH = "tutorial";
/** The sparql path. */
private final static String SPARQL_PATH = "sparql";
/** The tql path. */
private final static String TQL_PATH = "tql";
/** The default service path. */
private final static String DEFAULT_SERVICE = WEBQUERY_PATH;
/** The key to the bound host name in the attribute map of the servlet context. */
public final static String BOUND_HOST_NAME_KEY = "boundHostname";
/** Key to the bound server model uri in the attribute map of the servlet context. */
public final static String SERVER_MODEL_URI_KEY = "serverModelURI";
/** The HTTP server instance. */
private final Server httpServer;
/** The Public HTTP server instance. */
private final Server httpPublicServer;
/** The configuration for the server. */
private final MulgaraConfig config;
/** The name for the host. */
private String hostName;
/** The host server. This may contain information useful to services. */
private final EmbeddedMulgaraServer hostServer;
/**
* Creates the web services object.
* @param hostServer The Server that started these Web services.
* @param hostName The name of the HTTP host this object is setting up.
* @param config The configuration to use.
* @throws IOException Exception setting up with files or network.
* @throws SAXException Problem reading XML configurations.
* @throws ClassNotFoundException An expected class was not found.
* @throws NoSuchMethodException A configured class was not built as expected.
* @throws InvocationTargetException A configured class did not behave as expected.
* @throws IllegalAccessException A configured class was not accessible.
*/
public HttpServicesImpl(EmbeddedMulgaraServer hostServer, String hostName, MulgaraConfig config) throws IOException, SAXException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
this.hostServer = hostServer;
this.config = config;
this.hostName = hostName;
assert !config.getJetty().isDisabled();
// get servers as a pair so we can set them here. Needed because they are final.
Pair<Server,Server> servers = createHttpServers();
httpServer = servers.first();
httpPublicServer = servers.second();
}
/**
* @see org.mulgara.server.HttpServices#start()
*/
@SuppressWarnings("unchecked")
public void start() throws ExceptionList, Exception {
try {
if (httpServer != null) httpServer.start();
if (httpPublicServer != null) httpPublicServer.start();
} catch (MultiException e) {
throw new ExceptionList(e.getThrowables());
}
}
/**
* @see org.mulgara.server.HttpServices#stop()
*/
public void stop() throws Exception {
try {
if (httpServer != null) httpServer.stop();
} finally {
if (httpPublicServer != null) httpPublicServer.stop();
}
}
/**
* Creates an HTTP server.
* @return a pair of private/public servers.
* @throws IOException if the server configuration cannot be found
* @throws SAXException if the HTTP server configuration file is invalid
* @throws ClassNotFoundException if the HTTP server configuration file contains a reference to an unkown class
* @throws NoSuchMethodException if the HTTP server configuration file contains a reference to an unkown method
* @throws InvocationTargetException if an error ocurrs while trying to configure the HTTP server
* @throws IllegalAccessException If a class loaded by the server is accessed in an unexpected way.
*/
Pair<Server,Server> createHttpServers() throws IOException, SAXException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
if (logger.isDebugEnabled()) logger.debug("Creating HTTP server instance");
// Set the magic logging property for Jetty to use Log4j
System.setProperty(JettyLogger.LOGGING_CLASS_PROPERTY, JettyLogger.class.getCanonicalName());
JettyLogger.setEnabled(false);
org.eclipse.jetty.util.log.Log.setLog(new JettyLogger());
// create and register a new HTTP server
Server privateServer = buildAndConfigure(config.getJetty().getConnector(), ServerInfo.getHttpPort());
Server publicServer = buildAndConfigure(config.getJetty().getPublicConnector(), ServerInfo.getPublicHttpPort());
if (privateServer != null) {
// Accumulator for all the services
Map<String,String> privateServices = new HashMap<String,String>();
// start all the private configured services.
for (ContextStarter starter: getContextStarters()) {
try {
starter.fn(privateServer).addTo(privateServices);
} catch (IllegalStateException e) {
// not fatal, so just log the problem and go on
logger.warn("Unable to start web service", e.getCause());
}
}
// we have all the services, so now instantiate the service listing service
addWebServiceListingContext(privateServer, privateServices);
}
if (publicServer != null) {
// start the public contexts
for (ContextStarter starter: getPublicContextStarters()) {
try {
starter.fn(publicServer);
} catch (IllegalStateException e) {
logger.warn("Unable to start public web service", e.getCause());
}
}
}
// configure the private handlers
List<Handler> privateHandlers = Collections.emptyList();
if (privateServer != null) {
privateHandlers = Arrays.asList(privateServer.getChildHandlers());
configureHandlers(privateHandlers, config.getJetty().getConnector());
}
// configure the private handlers
List<Handler> publicHandlers = Collections.emptyList();
if (publicServer != null) {
publicHandlers = Arrays.asList(publicServer.getChildHandlers());
configureHandlers(publicHandlers, config.getJetty().getPublicConnector());
}
// get all the handlers in use by both servers
List<Handler> handlers = new ArrayList<Handler>(privateHandlers);
handlers.addAll(publicHandlers);
// add our class loader as the classloader of all contexts, unless this is a webapp in which case we wrap it
ClassLoader classLoader = this.getClass().getClassLoader();
for (Handler handler: handlers) {
if (handler instanceof WebAppContext) ((WebAppContext)handler).setClassLoader(new WebAppClassLoader(classLoader, (WebAppContext)handler));
else if (handler instanceof ContextHandler) ((ContextHandler)handler).setClassLoader(classLoader);
}
// return the servers
return new Pair<Server,Server>(privateServer, publicServer);
}
/**
* Create a server object, and configure it.
* @param cfg The Jetty configuration for the server.
* @return The created server.
* @throws UnknownHostException The configured host name is invalid.
*/
Server buildAndConfigure(JettyConnector cfg, int port) throws UnknownHostException {
Server s = null;
boolean disabled = (cfg != null && cfg.hasDisabled() && cfg.isDisabled());
if (!disabled) {
if (cfg != null) {
s = new Server();
addConnector(s, cfg, port);
} else {
s = new Server(port);
}
}
return s;
}
/**
* Configure all of the handlers with a connector configuration.
* @param handlers The handlers to configure.
* @param conCfg The connector configuration to use.
*/
void configureHandlers(List<Handler> handlers, JettyConnector conCfg) {
if (conCfg == null) return;
for (Handler handler: handlers) {
// expect each handler to be an instance of a ContextHandler, but we still check
if (handler instanceof ContextHandler) {
ContextHandler ctx = (ContextHandler)handler;
// test for parameters, and if present, then set on the handler
if (conCfg.hasMaxFormContentSize()) {
ctx.setMaxFormContentSize(conCfg.getMaxFormContentSize());
}
}
}
}
/**
* Creates a list of functions for starting contexts.
* <strong>This defines the list of services to be run.</strong>
* @return A list that can start all the configured contexts.
*/
private List<ContextStarter> getContextStarters() {
List<ContextStarter> starters = new ArrayList<ContextStarter>();
starters.add(new ContextStarter() { public Service fn(Server s) throws IOException {
return addWebServicesWebAppContext(s);
} });
starters.add(new ContextStarter() { public Service fn(Server s) throws IOException {
return addWebQueryContext(s, "User Interface", "org.mulgara.webquery.QueryServlet", WEBQUERY_PATH);
} });
starters.add(new ContextStarter() { public Service fn(Server s) throws IOException {
return addWebQueryContext(s, "User Tutorial", "org.mulgara.webquery.TutorialServlet", WEBTUTORIAL_PATH);
} });
// expect to get the following from a config file
// TODO: create a decent configuration object, instead of just handing out a Server
starters.add(new ContextStarter() { public Service fn(Server s) throws IOException {
return addServletContext(s, "org.mulgara.protocol.http.SparqlServlet", SPARQL_PATH, "SPARQL HTTP Service");
} });
starters.add(new ContextStarter() { public Service fn(Server s) throws IOException {
return addServletContext(s, "org.mulgara.protocol.http.TqlServlet", TQL_PATH, "TQL HTTP Service");
} });
return starters;
}
/**
* Creates a list of functions for starting public contexts.
* <strong>This defines the list of services to be run.</strong>
* @return A list that can start all the configured contexts.
*/
private List<ContextStarter> getPublicContextStarters() {
List<ContextStarter> starters = new ArrayList<ContextStarter>();
starters.add(new ContextStarter() { public Service fn(Server s) throws IOException {
return addServletContext(s, "org.mulgara.protocol.http.PublicSparqlServlet", SPARQL_PATH, "SPARQL HTTP Service");
} });
return starters;
}
/**
* Adds a listener to the <code>httpServer</code>. The listener is created and configured
* according to the Jetty configuration.
* @param httpServer the server to add the listener to
* @param jettyConfig The configuraton for the server. Do not read the port at this point.
* @param port The port to listen on. The configuration may have been overriden by this value.
* @throws UnknownHostException if an invalid hostname was specified in the Mulgara server configuration
*/
@SuppressWarnings("deprecation")
private void addConnector(Server httpServer, JettyConnector jettyConfig, int port) throws UnknownHostException {
if (httpServer == null) throw new IllegalArgumentException("Null \"httpServer\" parameter");
if (logger.isDebugEnabled()) logger.debug("Adding socket listener");
// create and configure a listener
AbstractConnector connector = new BlockingChannelConnector();
if ((hostName != null) && !hostName.equals("")) {
connector.setHost(hostName);
if (logger.isDebugEnabled()) logger.debug("Servlet container listening on host " + hostName);
} else {
hostName = EmbeddedMulgaraServer.getResolvedLocalHost();
if (logger.isDebugEnabled()) logger.debug("Servlet container listening on all host interfaces");
}
// Each connector will get its own thread pool, so that they may be configured separately.
// If a connector does not have its own thread pool, it inherits one from the server that it might
// share with other connectors.
QueuedThreadPool threadPool = new QueuedThreadPool();
if (jettyConfig.hasMaxThreads()) {
threadPool.setMaxThreads(jettyConfig.getMaxThreads());
}
connector.setThreadPool(threadPool);
connector.setPort(port);
if (jettyConfig.hasMaxIdleTimeMs()) connector.setMaxIdleTime(jettyConfig.getMaxIdleTimeMs());
if (jettyConfig.hasLowResourceMaxIdleTimeMs()) connector.setLowResourceMaxIdleTime(jettyConfig.getLowResourceMaxIdleTimeMs());
if (jettyConfig.hasAcceptors()) {
int acceptors = jettyConfig.getAcceptors();
// Acceptors are part of the thread pool, but they delegate handling of servlet
// requests to another thread in the pool. Therefore, the number of acceptors
// must be strictly less than the maximum number of threads in the pool.
int acceptorLimit = threadPool.getMaxThreads() - 1;
if (acceptors > acceptorLimit) {
logger.warn("Acceptor threads set beyond HTTP Server limits. Reducing from" + acceptors + " to " + acceptorLimit);
acceptors = acceptorLimit;
}
connector.setAcceptors(acceptors);
}
// add the listener to the http server
httpServer.addConnector(connector);
}
/**
* Creates the Mulgara Descriptor UI. Once created, a description of the created services is returned.
* @return A Service description of the Descriptor services that were started.
* @throws IOException if the driver WAR file is not readable
*/
private Service addWebServicesWebAppContext(Server server) throws IOException {
// get the URL to the WAR file
URL webServicesWebAppURL = ClassLoader.getSystemResource(WEBAPP_PATH + "/" + WEBSERVICES_WEBAPP);
if (webServicesWebAppURL == null) {
logger.warn("Couldn't find resource: " + WEBAPP_PATH + "/" + WEBSERVICES_WEBAPP);
return null;
}
String warPath = extractToTemp(WEBAPP_PATH + "/" + WEBSERVICES_WEBAPP);
// Add Descriptors and Axis
String webPath = "/" + WEBSERVICES_PATH;
WebAppContext descriptorWARContext = new WebAppContext(getHandlerCollection(server), warPath, webPath);
// make some attributes available
descriptorWARContext.setAttribute(BOUND_HOST_NAME_KEY, ServerInfo.getBoundHostname());
descriptorWARContext.setAttribute(SERVER_MODEL_URI_KEY, ServerInfo.getServerURI().toString());
// log that we're adding the test webapp context
if (logger.isDebugEnabled()) logger.debug("Added Web Services webapp context");
return new Service("Web Services", webPath);
}
/**
* Creates and registers a web servlet. The servlet will need to know the server name.
* @param server The server to connect the servlet to.
* @param name The human-readable name of the servlet to create.
* @param servletClass The name of the class that will perform the servlet functions.
* @param servletPath The path on the server for the servlet to be loaded onto.
* @return A description of the servlet that was set up.
* @throws IOException if the servlet cannot talk to the network.
*/
private Service addWebQueryContext(Server server, String name, String servletClass, String servletPath) throws IOException {
if (logger.isDebugEnabled()) logger.debug("Adding Web servlet context: " + name);
// create the web context
try {
String rmiName = hostServer.getServerName();
Servlet servlet = (Servlet)Reflect.newInstance(Class.forName(servletClass), hostName, rmiName, hostServer);
String webPath = "/" + servletPath;
new org.eclipse.jetty.servlet.ServletContextHandler(getHandlerCollection(server), webPath, SESSIONS).addServlet(new ServletHolder(servlet), "/*");
return new Service(name, webPath);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Not configured to use the requested servlet: " + name + "(" + servletClass + ")");
}
}
/**
* Creates a servlet that requires a Server as a constructor parameter. This is similar to {@link #addWebQueryContext(Server, String, String, String)}
* except that the servlet does not need to know the server name.
* @param server The server to connect the servlet to.
* @param servletClass The name of the servlet class.
* @param path A relative HTTP path for attaching the servlet.
* @param description A description for the servlet.
* @return The Service describing the new servlet.
* @throws IOException Due to problems with the file system or the network.
* @throws IllegalStateException if an unavailable servlet has been requested.
*/
private Service addServletContext(Server server, String servletClass, String path, String description) throws IOException {
if (logger.isDebugEnabled()) logger.debug("Adding " + description + " servlet context");
// create the web query context
try {
Servlet servlet = (Servlet)Reflect.newInstance(Class.forName(servletClass), hostServer);
String webPath = "/" + path;
new org.eclipse.jetty.servlet.ServletContextHandler(getHandlerCollection(server), webPath, SESSIONS).addServlet(new ServletHolder(servlet), "/*");
return new Service(description, webPath);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Not configured to use the requested servlet: " + description);
}
}
/**
* Creates the servlet used to list the other servlets.
* @param server The server to register this servlet with.
* @param services The list of service names and paths.
* @throws IOException If the servlet cannot talk to the network.
*/
private void addWebServiceListingContext(Server server, Map<String,String> services) throws IOException {
if (logger.isDebugEnabled()) logger.debug("Adding the service lister context");
Servlet servlet = new ServiceListingServlet(services, "/" + DEFAULT_SERVICE);
new org.eclipse.jetty.servlet.ServletContextHandler(getHandlerCollection(server), "/", SESSIONS).addServlet(new ServletHolder(servlet), "/*");
}
/**
* Retrieves a handler collection from the server, setting it if the server does not hold a collection.
* @param server The server to get a handler collection from.
* @return A handler collection for the server.
*/
private HandlerContainer getHandlerCollection(Server server) {
HandlerCollection handlerCollection;
Handler handler = server.getHandler();
if (handler instanceof HandlerCollection) {
handlerCollection = (HandlerCollection)handler;
} else {
handlerCollection = new HandlerCollection();
if (handler != null) handlerCollection.addHandler(handler);
server.setHandler(handlerCollection);
}
return handlerCollection;
}
/**
* Extracts a resource from the environment (a jar in the classpath) and writes
* this to a file in the working temporary directory.
* @param resourceName The name of the resource. This is a relative file path in the jar file.
* @return The absolute path of the file the resource is extracted to, or <code>null</code>
* if the resource does not exist.
* @throws IOException If there was an error reading the resource, or writing to the extracted file.
*/
private String extractToTemp(String resourceName) throws IOException {
// Find the resource
URL resourceUrl = ClassLoader.getSystemResource(resourceName);
if (resourceUrl == null) return null;
// open the resource and the file where it will be copied to
File outFile = new File(TempDir.getTempDir(), new File(resourceName).getName());
logger.info("Extracting: " + resourceUrl + " to " + outFile);
InputStream in = null;
OutputStream out = null;
try {
in = resourceUrl.openStream();
out = new FileOutputStream(outFile);
// loop to copy from the resource to the output file
byte[] buffer = new byte[10240];
int len;
while ((len = in.read(buffer)) >= 0) out.write(buffer, 0, len);
} finally {
if (in != null) in.close();
if (out != null) out.close();
}
// return the file that the resource was extracted to
return outFile.getAbsolutePath();
}
}