All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jena.fuseki.main.JettyServer Maven / Gradle / Ivy

There is a newer version: 5.2.0
Show newest version
/*
 * 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.main;

import static java.util.Objects.requireNonNull;
import static org.apache.jena.fuseki.Fuseki.serverLog;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.servlet.Filter;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServlet;
import org.apache.jena.atlas.lib.FileOps;
import org.apache.jena.atlas.lib.Pair;
import org.apache.jena.fuseki.Fuseki;
import org.apache.jena.fuseki.FusekiConfigException;
import org.apache.jena.fuseki.main.sys.JettyLib;
import org.apache.jena.fuseki.server.DataAccessPointRegistry;
import org.apache.jena.fuseki.server.OperationRegistry;
import org.apache.jena.fuseki.servlets.ActionBase;
import org.apache.jena.riot.WebContent;
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Jetty server for servlets, including being able to run Fuseki {@link ActionBase} derived servlets.
 * Static RDF types by file extension can be enabled.
 */
public class JettyServer {
    // Possibility: Use this for the super class of FusekiServer or within FusekiServer.jettyServer
    // as implementation inheritance.
    // Caution : there are small differences e.g. in building where order matters.

    private static Logger LOG = LoggerFactory.getLogger("HTTP");

    protected final Server server;
    protected int port;

    public static Builder create() {
        return new Builder();
    }

    protected JettyServer(int port, Server server) {
        this.server = server;
        this.port = port;
    }

    /**
     * Return the port begin used.
     * This will be the give port, which defaults to 3330, or
     * the one actually allocated if the port was 0 ("choose a free port").
     */
    public int getPort() {
        return port;
    }

    /** Get the underlying Jetty server which has also been set up. */
    public Server getJettyServer() {
        return server;
    }

    /** Get the {@link ServletContext}.
     * Adding new servlets is possible with care.
     */
    public ServletContext getServletContext() {
        return ((ServletContextHandler)server.getHandler()).getServletContext();
    }

    /** Start the server - the server continues to run after this call returns.
     *  To synchronise with the server stopping, call {@link #join}.
     */
    public JettyServer start() {
        try { server.start(); }
        catch (Exception e) { throw new RuntimeException(e); }
        if ( port == 0 )
            port = ((ServerConnector)server.getConnectors()[0]).getLocalPort();
        logStart();
        return this;
    }

    protected void logStart() {
        LOG.info("Start (port="+port+")");
    }

    /** Stop the server. */
    public void stop() {
        logStop();
        try { server.stop(); }
        catch (Exception e) { throw new RuntimeException(e); }
    }

    protected void logStop() {
        LOG.info("Stop (port="+port+")");
    }

    /** Wait for the server to exit. This call is blocking. */
    public void join() {
        try { server.join(); }
        catch (Exception e) { throw new RuntimeException(e); }
    }

    /** Simple error handler - always text/plain. */
    public static class PlainErrorHandler extends ErrorHandler {
        public PlainErrorHandler() {}

        @Override
        protected boolean generateAcceptableResponse(Request request, Response response, Callback callback, String contentType, List charsets, int code, String message, Throwable cause) throws IOException {
            System.err.println(">> plain:generateAcceptableResponse");
            // Force text/plain
            // https://github.com/eclipse/jetty.project/issues/10474 (Bug in 12.0.0, 12.0.1 for applicatipon/json)
            if ( contentType != null && contentType.equals(WebContent.contentTypeJSON) )
                contentType = MimeTypes.Type.TEXT_PLAIN.asString();

            try {
                boolean b =
                        super.generateAcceptableResponse(request, response, callback,
                                                         //"text/plain", List.of(StandardCharsets.UTF_8),
                                                         contentType, charsets,
                                                         code, message, cause);
                System.err.println("<< plain:generateAcceptableResponse: "+b);
                return b;

            } catch (IllegalStateException ex) {
                System.err.println("<< plain:generateAcceptableResponse: IllegalStateException");
                ex.printStackTrace();
                return true;
            }
        }

        @Override
        protected void generateResponse(Request request, Response response, int code, String message, Throwable cause, Callback callback) throws IOException {
            generateAcceptableResponse(request, response, callback, WebContent.contentTypeTextPlain, List.of(StandardCharsets.UTF_8), code, message, cause);
        }

//        @Override
//        protected void writeErrorPlain(Request request, PrintWriter writer, int code, String message, Throwable cause, boolean showStacks) {
//            writer.write("HTTP ERROR ");
//            writer.write(Integer.toString(code));
//            writer.write(' ');
//            writer.write(StringUtil.sanitizeXmlString(message));
//            writer.write("\n");
//            writer.printf("URI: %s%n", request.getHttpURI());
//            writer.printf("STATUS: %s%n", code);
//            writer.printf("MESSAGE: %s%n", message);
//            while (cause != null) {
//                writer.printf("CAUSED BY %s%n", cause);
//                if (showStacks)
//                    cause.printStackTrace(writer);
//                cause = cause.getCause();
//            }
//        }
    }

    public static class JettyConfigException extends FusekiConfigException {
        public JettyConfigException(String msg) { super(msg); }
    }

    public static class Builder {
        private int                     port               = -1;
        private int                     minThreads         = -1;
        private int                     maxThreads         = -1;
        private boolean                 loopback           = false;
        private String                  jettyServerConfig  = null;
        protected boolean               verbose            = false;
        // Other servlets to add.
        private List> servlets   = new ArrayList<>();
        private List> filters         = new ArrayList<>();

        private String                  contextPath        = "/";
        private String                  servletContextName = "Jetty";
        private String                  staticContentDir   = null;
        private SecurityHandler         securityHandler    = null;
        private ErrorHandler            errorHandler       = new PlainErrorHandler();
        private Map     servletAttr        = new HashMap<>();

        public Builder() {}

        /** Set the port to run on. */
        public Builder port(int port) {
            if ( port < 0 )
                throw new IllegalArgumentException("Illegal port="+port+" : Port must be greater than or equal to zero.");
            this.port = port;
            return this;
        }

        /**
         * Build the server using a Jetty configuration file.
         * See Jetty/Reference/jetty.xml_syntax
         * This is instead of any other server settings such as port or https.
         */
        public Builder jettyServerConfig(String filename) {
            requireNonNull(filename, "filename");
            if ( ! FileOps.exists(filename) )
                throw new JettyConfigException("File not found: "+filename);
            this.jettyServerConfig = filename;
            return this;
        }

        /**
         * Context path.  If it's "/" then Server URL will look like
         * "http://host:port/" else "http://host:port/path/"
         * (or no port if :80).
         */
        public Builder contextPath(String path) {
            requireNonNull(path, "path");
            this.contextPath = path;
            return this;
        }

        /**
         * ServletContextName.
         */
        public Builder servletContextName(String name) {
            requireNonNull(name, "name");
            this.servletContextName = name;
            return this;
        }

        /** Restrict the server to only responding to the localhost interface. */
        public Builder loopback(boolean loopback) {
            this.loopback = loopback;
            return this;
        }

        /** Set the location (filing system directory) to serve static file from. */
        public Builder staticFileBase(String directory) {
            requireNonNull(directory, "directory");
            this.staticContentDir = directory;
            return this;
        }

        /** Set a Jetty SecurityHandler.
         * 

* By default, the server runs with no security. * This is more for using the basic server for testing. * The full Fuseki server provides security with Apache Shiro * and a defensive reverse proxy (e.g. Apache httpd) in front of the Jetty server * can also be used, which provides a wide varity of proven security options. */ public Builder securityHandler(SecurityHandler securityHandler) { requireNonNull(securityHandler, "securityHandler"); this.securityHandler = securityHandler; return this; } /** Set an {@link ErrorHandler}. *

* By default, the server runs with error handle that prints the code and message. */ public Builder errorHandler(ErrorHandler errorHandler) { requireNonNull(errorHandler, "securityHandler"); this.errorHandler = errorHandler; return this; } /** Set verbose logging */ public Builder verbose(boolean verbose) { this.verbose = verbose; return this; } /** * Set the number threads used by Jetty. This uses a {@code org.eclipse.jetty.util.thread.QueuedThreadPool} provided by Jetty. *

* Argument order is (minThreads, maxThreads). *

    *
  • Use (-1,-1) for Jetty "default". The Jetty 9.4 defaults are (min=8,max=200). *
  • If (min != -1, max is -1) then the default max is 20. *
  • If (min is -1, max != -1) then the default min is 2. *
*/ public Builder numServerThreads(int minThreads, int maxThreads) { if ( minThreads >= 0 && maxThreads > 0 ) { if ( minThreads > maxThreads ) throw new JettyConfigException(String.format("Bad thread setting: (min=%d, max=%d)", minThreads, maxThreads)); } this.minThreads = minThreads; this.maxThreads = maxThreads; return this; } /** * Set the maximum number threads used by Jetty. * This is equivalent to {@code numServerThreads(-1, maxThreads)} * and overrides any previous setting of the maximum number of threads. * In development or in embedded use, limiting the maximum threads can be useful. */ public Builder maxServerThreads(int maxThreads) { if ( minThreads > maxThreads ) throw new JettyConfigException(String.format("Bad thread setting: (min=%d, max=%d)", minThreads, maxThreads)); numServerThreads(minThreads, maxThreads); return this; } /** * Add the given servlet with the pathSpec. These are added so that they are * before the static content handler (which is the last servlet) * used for {@link #staticFileBase(String)}. */ public Builder addServlet(String pathSpec, HttpServlet servlet) { requireNonNull(pathSpec, "pathSpec"); requireNonNull(servlet, "servlet"); servlets.add(Pair.create(pathSpec, servlet)); return this; } /** * Add a servlet attribute. Pass a value of null to remove any existing binding. */ public Builder addServletAttribute(String attrName, Object value) { requireNonNull(attrName, "attrName"); if ( value != null ) servletAttr.put(attrName, value); else servletAttr.remove(attrName); return this; } /** * Add the given filter with the pathSpec. * It is applied to all dispatch types. */ public Builder addFilter(String pathSpec, Filter filter) { requireNonNull(pathSpec, "pathSpec"); requireNonNull(filter, "filter"); filters.add(Pair.create(pathSpec, filter)); return this; } /** * Build a server according to the current description. */ public JettyServer build() { ServletContextHandler handler = buildServletContext(); // Use HandlerCollection for several ServletContextHandlers and thus several ServletContext. Server server = jettyServerConfig != null ? jettyServer(jettyServerConfig) : jettyServer(minThreads, maxThreads); serverAddConnectors(server, port, loopback); server.setHandler(handler); return new JettyServer(port, server); } /** Build a ServletContextHandler : one servlet context */ private ServletContextHandler buildServletContext() { ServletContextHandler handler = buildServletContext(contextPath); ServletContext cxt = handler.getServletContext(); adjustForFuseki(cxt); servletAttr.forEach((n,v)->cxt.setAttribute(n, v)); servletsAndFilters(handler); return handler; } private void adjustForFuseki(ServletContext cxt) { // For Fuseki servlets added directly. // This enables servlets inheriting from {@link ActionBase} to work in the // plain Jetty server, e.g. to use Fuseki logging. try { Fuseki.setVerbose(cxt, verbose); OperationRegistry.set(cxt, OperationRegistry.createEmpty()); DataAccessPointRegistry.set(cxt, new DataAccessPointRegistry()); } catch (NoClassDefFoundError err) { LOG.info("Fuseki classes not found"); } } /** Build a ServletContextHandler. */ private ServletContextHandler buildServletContext(String contextPath) { if ( contextPath == null || contextPath.isEmpty() ) contextPath = "/"; else if ( !contextPath.startsWith("/") ) contextPath = "/" + contextPath; ServletContextHandler context = new ServletContextHandler(); context.setDisplayName(servletContextName); context.setErrorHandler(errorHandler); context.setContextPath(contextPath); if ( securityHandler != null ) context.setSecurityHandler(securityHandler); return context; } /** Add servlets and servlet filters */ private void servletsAndFilters(ServletContextHandler context) { servlets.forEach(p-> addServlet(context, p.getLeft(), p.getRight()) ); filters.forEach (p-> addFilter (context, p.getLeft(), p.getRight()) ); if ( staticContentDir != null ) { DefaultServlet staticServlet = new DefaultServlet(); ServletHolder staticContent = new ServletHolder(staticServlet); staticContent.setInitParameter("baseResource", staticContentDir); context.addServlet(staticContent, "/"); } } protected static void addServlet(ServletContextHandler context, String pathspec, HttpServlet httpServlet) { ServletHolder sh = new ServletHolder(httpServlet); context.addServlet(sh, pathspec); } protected void addFilter(ServletContextHandler context, String pathspec, Filter filter) { FilterHolder h = new FilterHolder(filter); context.addFilter(h, pathspec, null); } } // Jetty Server public static Server jettyServer(String jettyConfig) { try { Server server = new Server(); Resource configXml = JettyLib.newResource(jettyConfig); XmlConfiguration configuration = new XmlConfiguration(configXml); configuration.configure(server); return server; } catch (Exception ex) { serverLog.error("JettyServer: Failed to configure server: " + ex.getMessage(), ex); throw new JettyConfigException("Failed to configure a server using configuration file '" + jettyConfig + "'"); } } public static Server jettyServer(int minThreads, int maxThreads) { ThreadPool threadPool = null; // Jetty 9.4 and 12.0 : the Jetty default is max=200, min=8 if ( minThreads < 0 ) minThreads = 2; if ( maxThreads < 0 ) maxThreads = 20; maxThreads = Math.max(minThreads, maxThreads); // Args reversed: Jetty uses (max,min) threadPool = new QueuedThreadPool(maxThreads, minThreads); Server server = new Server(threadPool); return server; } private static void serverAddConnectors(Server server, int port, boolean loopback) { HttpConnectionFactory f1 = new HttpConnectionFactory(); //f1.getHttpConfiguration().setRequestHeaderSize(512 * 1024); //f1.getHttpConfiguration().setOutputBufferSize(1024 * 1024); f1.getHttpConfiguration().setSendServerVersion(false); ServerConnector connector = new ServerConnector(server, f1); connector.setPort(port); server.addConnector(connector); if ( loopback ) connector.setHost("localhost"); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy