/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.jena.fuseki.jetty ;
import static java.lang.String.format ;
import static org.apache.jena.fuseki.Fuseki.serverLog ;
import java.io.FileInputStream ;
import javax.servlet.ServletContext ;
import org.apache.jena.atlas.lib.DateTimeUtils ;
import org.apache.jena.atlas.lib.FileOps ;
import org.apache.jena.fuseki.Fuseki ;
import org.apache.jena.fuseki.FusekiException ;
import org.apache.jena.fuseki.mgt.MgtJMX ;
import org.apache.jena.fuseki.server.DataAccessPointRegistry ;
import org.apache.jena.fuseki.server.FusekiEnv ;
import org.eclipse.jetty.security.* ;
import org.eclipse.jetty.security.authentication.BasicAuthenticator ;
import org.eclipse.jetty.server.HttpConnectionFactory ;
import org.eclipse.jetty.server.Server ;
import org.eclipse.jetty.server.ServerConnector ;
import org.eclipse.jetty.server.handler.gzip.GzipHandler ;
import org.eclipse.jetty.servlet.ServletContextHandler ;
import org.eclipse.jetty.util.security.Constraint ;
import org.eclipse.jetty.webapp.WebAppContext ;
import org.eclipse.jetty.xml.XmlConfiguration ;
/** Standalone server, not run as a WAR file.
* Used in testing and development.
*
* SPARQLServer is the Jena server instance which wraps/utilizes
* {@link org.eclipse.jetty.server.Server}. This class provides
* immediate access to the {@link org.eclipse.jetty.server.Server#start()} and
* {@link org.eclipse.jetty.server.Server#stop()} commands as well as obtaining
* instances of the server and server configuration. Finally we can obtain
* instances of {@link org.apache.jena.fuseki.jetty.JettyServerConfig}.
*/
public class JettyFuseki {
// Jetty specific.
// This class is becoming less important - it now sets up a Jetty server for in-process use
// either for the command line in development
// and in testing but not direct webapp deployments.
static { Fuseki.init() ; }
public static JettyFuseki instance = null ;
private ServerConnector serverConnector = null ;
// If a separate ...
private ServerConnector mgtConnector = null ;
private JettyServerConfig serverConfig ;
// The jetty server.
private Server server = null ;
private ServletContext servletContext = null ;
// webapp setup - standard maven layout
public static String contextpath = "/" ;
// Standalone jar
public static final String resourceBase1 = "webapp" ;
// Development
public static final String resourceBase2 = "src/main/webapp" ;
/**
* Default setup which requires a {@link org.apache.jena.fuseki.jetty.JettyServerConfig}
* object as input. We use this config to pass in the command line arguments for dataset,
* name etc.
* @param config
*/
public static void initializeServer(JettyServerConfig config) {
// Currently server-wide.
Fuseki.verboseLogging = config.verboseLogging ;
instance = new JettyFuseki(config) ;
}
private JettyFuseki(JettyServerConfig config) {
this.serverConfig = config ;
buildServerWebapp(serverConfig.contextPath, serverConfig.jettyConfigFile) ;
if ( mgtConnector == null )
mgtConnector = serverConnector ;
if ( config.enableCompression ) {
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.setHandler(server.getHandler());
server.setHandler(gzipHandler);
}
}
/**
* Initialize the {@link JettyFuseki} instance.
*/
public void start() {
String version = Fuseki.VERSION ;
String buildDate = Fuseki.BUILD_DATE ;
if ( version != null && version.equals("${project.version}") )
version = null ;
if ( buildDate != null && buildDate.equals("${build.time.xsd}") )
buildDate = DateTimeUtils.nowAsXSDDateTimeString() ;
if ( version != null ) {
if ( Fuseki.developmentMode && buildDate != null )
serverLog.info(format("%s %s %s", Fuseki.NAME, version, buildDate)) ;
else
serverLog.info(format("%s %s", Fuseki.NAME, version)) ;
}
// This does not get set usefully for Jetty as we use it.
// String jettyVersion = org.eclipse.jetty.server.Server.getVersion() ;
// serverLog.info(format("Jetty %s",jettyVersion)) ;
String host = serverConnector.getHost() ;
if ( host != null )
serverLog.info("Incoming connections limited to " + host) ;
try {
server.start() ;
} catch (java.net.BindException ex) {
serverLog.error("SPARQLServer (port="+serverConnector.getPort()+"): Failed to start server: " + ex.getMessage()) ;
System.exit(1) ;
} catch (Exception ex) {
serverLog.error("SPARQLServer: Failed to start server: " + ex.getMessage(), ex) ;
System.exit(1) ;
}
String now = DateTimeUtils.nowAsString() ;
serverLog.info(format("Started %s on port %d", now, serverConnector.getPort())) ;
}
/**
* Sync with the {@link JettyFuseki} instance.
* Returns only if the server exits cleanly
*/
public void join() {
try {
server.join() ;
} catch (InterruptedException ex) { }
}
/**
* Stop the {@link JettyFuseki} instance.
*/
public void stop() {
String now = DateTimeUtils.nowAsString() ;
serverLog.info(format("Stopped %s on port %d", now, serverConnector.getPort())) ;
try {
server.stop() ;
} catch (Exception ex) {
Fuseki.serverLog.warn("SPARQLServer: Exception while stopping server: " + ex.getMessage(), ex) ;
}
MgtJMX.removeJMX() ;
}
public static WebAppContext createWebApp(String contextPath) {
FusekiEnv.setEnvironment();
WebAppContext webapp = new WebAppContext();
webapp.getServletContext().getContextHandler().setMaxFormContentSize(10 * 1000 * 1000) ;
// Hunt for the webapp for the standalone jar (or development system).
// Note that Path FUSEKI_HOME is not initialized until the webapp starts
// so it is not available here.
String resourceBase3 = null ;
String resourceBase4 = null ;
if ( FusekiEnv.FUSEKI_HOME != null ) {
String HOME = FusekiEnv.FUSEKI_HOME.toString() ;
resourceBase3 = HOME+"/"+resourceBase1 ;
resourceBase4 = HOME+"/"+resourceBase2 ;
}
String resourceBase = tryResourceBase(resourceBase1, null) ;
resourceBase = tryResourceBase(resourceBase2, resourceBase) ;
resourceBase = tryResourceBase(resourceBase3, resourceBase) ;
resourceBase = tryResourceBase(resourceBase4, resourceBase) ;
if ( resourceBase == null ) {
if ( resourceBase3 == null )
Fuseki.serverLog.error("Can't find resourceBase (tried "+resourceBase1+" and "+resourceBase2+")") ;
else
Fuseki.serverLog.error("Can't find resourceBase (tried "+resourceBase1+", "+resourceBase2+", "+resourceBase3+" and "+resourceBase4+")") ;
Fuseki.serverLog.error("Failed to start") ;
throw new FusekiException("Failed to start") ;
}
webapp.setDescriptor(resourceBase+"/WEB-INF/web.xml");
webapp.setResourceBase(resourceBase);
webapp.setContextPath(contextPath);
//-- Jetty setup for the ServletContext logger.
// The name of the Jetty-allocated slf4j/log4j logger is
// the display name or, if null, the context path name.
// It is set, without checking for a previous call of setLogger in "doStart"
// which happens during server startup.
// This the name of the ServletContext logger as well
webapp.setDisplayName(Fuseki.servletRequestLogName);
webapp.setParentLoaderPriority(true); // Normal Java classloader behaviour.
webapp.setErrorHandler(new FusekiErrorHandler()) ;
return webapp ;
}
public static String getenv(String name) {
String x = System.getenv(name) ;
if ( x == null )
x = System.getProperty(name) ;
return x ;
}
public DataAccessPointRegistry getDataAccessPointRegistry() {
return DataAccessPointRegistry.get(servletContext) ;
}
private static String tryResourceBase(String maybeResourceBase, String currentResourceBase) {
if ( currentResourceBase != null )
return currentResourceBase ;
if ( maybeResourceBase != null && FileOps.exists(maybeResourceBase) )
return maybeResourceBase ;
return currentResourceBase ;
}
private void buildServerWebapp(String contextPath, String jettyConfig) {
if ( jettyConfig != null )
// --jetty-config=jetty-fuseki.xml
// for detailed configuration of the server using Jetty features.
configServer(jettyConfig) ;
else
defaultServerConfig(serverConfig.port, serverConfig.loopback) ;
WebAppContext webapp = createWebApp(contextPath) ;
servletContext = webapp.getServletContext() ;
server.setHandler(webapp) ;
// Replaced by Shiro.
if ( jettyConfig == null && serverConfig.authConfigFile != null )
security(webapp, serverConfig.authConfigFile) ;
}
// This is now provided by Shiro.
private static void security(ServletContextHandler context, String authfile) {
Constraint constraint = new Constraint() ;
constraint.setName(Constraint.__BASIC_AUTH) ;
constraint.setRoles(new String[]{"fuseki"}) ;
constraint.setAuthenticate(true) ;
ConstraintMapping mapping = new ConstraintMapping() ;
mapping.setConstraint(constraint) ;
mapping.setPathSpec("/*") ;
IdentityService identService = new DefaultIdentityService() ;
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler() ;
securityHandler.addConstraintMapping(mapping) ;
securityHandler.setIdentityService(identService) ;
HashLoginService loginService = new HashLoginService("Fuseki Authentication", authfile) ;
loginService.setIdentityService(identService) ;
securityHandler.setLoginService(loginService) ;
securityHandler.setAuthenticator(new BasicAuthenticator()) ;
context.setSecurityHandler(securityHandler) ;
serverLog.debug("Basic Auth Configuration = " + authfile) ;
}
private void configServer(String jettyConfig) {
try {
serverLog.info("Jetty server config file = " + jettyConfig) ;
server = new Server() ;
XmlConfiguration configuration = new XmlConfiguration(new FileInputStream(jettyConfig)) ;
configuration.configure(server) ;
serverConnector = (ServerConnector)server.getConnectors()[0] ;
} catch (Exception ex) {
serverLog.error("SPARQLServer: Failed to configure server: " + ex.getMessage(), ex) ;
throw new FusekiException("Failed to configure a server using configuration file '" + jettyConfig + "'") ;
}
}
private void defaultServerConfig(int port, boolean loopback) {
server = new Server() ;
HttpConnectionFactory f1 = new HttpConnectionFactory() ;
// Some people do try very large operations ... really, should use POST.
f1.getHttpConfiguration().setRequestHeaderSize(512 * 1024);
f1.getHttpConfiguration().setOutputBufferSize(5 * 1024 * 1024) ;
// Do not add "Server: Jetty(....) when not a development system.
if ( ! Fuseki.outputJettyServerHeader )
f1.getHttpConfiguration().setSendServerVersion(false) ;
// https is better done with a Jetty configuration file
// because there are several things to configure.
// See "examples/fuseki-jetty-https.xml"
ServerConnector connector = new ServerConnector(server, f1) ;
connector.setPort(port) ;
server.addConnector(connector);
if ( loopback )
connector.setHost("localhost");
serverConnector = connector ;
}
}