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

org.neo4j.server.web.Jetty9WebServer Maven / Gradle / Ivy

There is a newer version: 5.26.1
Show newest version
/*
 * Copyright (c) 2002-2016 "Neo Technology,"
 * Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.server.web;

import java.io.IOException;
import java.net.BindException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.function.Consumer;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SessionManager;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.MovedContextHandler;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;

import org.neo4j.bolt.security.ssl.KeyStoreInformation;
import org.neo4j.helpers.HostnamePort;
import org.neo4j.helpers.PortBindException;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.logging.Log;
import org.neo4j.logging.LogProvider;
import org.neo4j.server.database.InjectableProvider;
import org.neo4j.server.plugins.Injectable;
import org.neo4j.server.security.ssl.SslSocketConnectorFactory;

import static java.lang.String.format;

/**
 * This class handles the configuration and runtime management of a Jetty web server. The server is restartable.
 *
 * TODO: it really should be split up into a builder that returns a Closeable, to separate between the conf and runtime part.
 */
public class Jetty9WebServer implements WebServer
{
    private boolean wadlEnabled;
    private Collection> defaultInjectables;
    private Consumer jettyCreatedCallback;
    private RequestLog requestLog;

    private static class FilterDefinition
    {
        private final Filter filter;
        private final String pathSpec;

        public FilterDefinition( Filter filter, String pathSpec )
        {
            this.filter = filter;
            this.pathSpec = pathSpec;
        }

        public boolean matches( Filter filter, String pathSpec )
        {
            return filter == this.filter && pathSpec.equals( this.pathSpec );
        }

        public Filter getFilter()
        {
            return filter;
        }

        public String getPathSpec()
        {
            return pathSpec;
        }
    }

    public static final HostnamePort DEFAULT_ADDRESS = new HostnamePort( "0.0.0.0", 80 );

    private Server jetty;
    private HandlerCollection handlers;
    private HostnamePort jettyAddress = DEFAULT_ADDRESS;
    private Optional jettyHttpsAddress = Optional.empty();

    private final HashMap staticContent = new HashMap<>();
    private final Map jaxRSPackages =
            new HashMap<>();
    private final Map jaxRSClasses =
            new HashMap<>();
    private final List filters = new ArrayList<>();

    private int jettyMaxThreads = 1;
    private KeyStoreInformation httpsCertificateInformation = null;
    private final SslSocketConnectorFactory sslSocketFactory;
    private final HttpConnectorFactory connectorFactory;
    private final Log log;

    public Jetty9WebServer( LogProvider logProvider, Config config )
    {
        this.log = logProvider.getLog( getClass() );
        sslSocketFactory = new SslSocketConnectorFactory(config);
        connectorFactory = new HttpConnectorFactory(config);
    }

    @Override
    public void start() throws Exception
    {
        if ( jetty == null )
        {
            JettyThreadCalculator jettyThreadCalculator = new JettyThreadCalculator( jettyMaxThreads );

            jetty = new Server( createQueuedThreadPool( jettyThreadCalculator ) );
            jetty.addConnector( connectorFactory.createConnector( jetty, jettyAddress, jettyThreadCalculator ) );

            jettyHttpsAddress.ifPresent( ( address ) ->
            {
                if ( httpsCertificateInformation == null )
                {
                    throw new RuntimeException( "HTTPS set to enabled, but no HTTPS configuration provided" );
                }
                jetty.addConnector( sslSocketFactory.createConnector(
                        jetty, httpsCertificateInformation, address, jettyThreadCalculator ) );
            } );

            if ( jettyCreatedCallback != null )
            {
                jettyCreatedCallback.accept( jetty );
            }
        }

        handlers = new HandlerList();
        jetty.setHandler( handlers );
        handlers.addHandler( new MovedContextHandler() );

        loadAllMounts();

        if ( requestLog != null )
        {
            loadRequestLogging();
        }

        startJetty();
    }

    private QueuedThreadPool createQueuedThreadPool( JettyThreadCalculator jtc )
    {
        BlockingQueue queue = new BlockingArrayQueue<>( jtc.getMinThreads(), jtc.getMinThreads(), jtc.getMaxCapacity() );
        return new QueuedThreadPool( jtc.getMaxThreads(), jtc.getMinThreads(), 60000, queue );
    }

    @Override
    public void stop()
    {
        if ( jetty != null )
        {
            try
            {
                jetty.stop();
            }
            catch ( Exception e )
            {
                throw new RuntimeException( e );
            }
            try
            {
                jetty.join();
            }
            catch ( InterruptedException e )
            {
                log.warn( "Interrupted while waiting for Jetty to stop" );
            }
            jetty = null;
        }
    }

    @Override
    public void setAddress( HostnamePort address )
    {
        jettyAddress = address;
    }

    @Override
    public void setMaxThreads( int maxThreads )
    {
        jettyMaxThreads = maxThreads;
    }

    @Override
    public void addJAXRSPackages( List packageNames, String mountPoint, Collection> injectables )
    {
        // We don't want absolute URIs at this point
        mountPoint = ensureRelativeUri( mountPoint );
        mountPoint = trimTrailingSlashToKeepJettyHappy( mountPoint );

        JaxRsServletHolderFactory factory = jaxRSPackages.get( mountPoint );
        if ( factory == null )
        {
            factory = new JaxRsServletHolderFactory.Packages();
            jaxRSPackages.put( mountPoint, factory );
        }
        factory.add( packageNames, injectables );

        log.debug( "Adding JAXRS packages %s at [%s]", packageNames, mountPoint );
    }

    @Override
    public void addJAXRSClasses( List classNames, String mountPoint, Collection> injectables )
    {
        // We don't want absolute URIs at this point
        mountPoint = ensureRelativeUri( mountPoint );
        mountPoint = trimTrailingSlashToKeepJettyHappy( mountPoint );

        JaxRsServletHolderFactory factory = jaxRSClasses.get( mountPoint );
        if ( factory == null )
        {
            factory = new JaxRsServletHolderFactory.Classes();
            jaxRSClasses.put( mountPoint, factory );
        }
        factory.add( classNames, injectables );

        log.debug( "Adding JAXRS classes %s at [%s]", classNames, mountPoint );
    }

    @Override
    public void setWadlEnabled( boolean wadlEnabled )
    {
        this.wadlEnabled = wadlEnabled;
    }

    @Override
    public void setDefaultInjectables( Collection> defaultInjectables )
    {
        this.defaultInjectables = defaultInjectables;
    }

    public void setJettyCreatedCallback( Consumer callback )
    {
        this.jettyCreatedCallback = callback;
    }

    @Override
    public void removeJAXRSPackages( List packageNames, String serverMountPoint )
    {
        JaxRsServletHolderFactory factory = jaxRSPackages.get( serverMountPoint );
        if ( factory != null )
        {
            factory.remove( packageNames );
        }
    }

    @Override
    public void removeJAXRSClasses( List classNames, String serverMountPoint )
    {
        JaxRsServletHolderFactory factory = jaxRSClasses.get( serverMountPoint );
        if ( factory != null )
        {
            factory.remove( classNames );
        }
    }

    @Override
    public void addFilter( Filter filter, String pathSpec )
    {
        filters.add( new FilterDefinition( filter, pathSpec ) );
    }

    @Override
    public void removeFilter( Filter filter, String pathSpec )
    {
        Iterator iter = filters.iterator();
        while ( iter.hasNext() )
        {
            FilterDefinition current = iter.next();
            if ( current.matches( filter, pathSpec ) )
            {
                iter.remove();
            }
        }
    }

    @Override
    public void addStaticContent( String contentLocation, String serverMountPoint )
    {
        staticContent.put( serverMountPoint, contentLocation );
    }

    @Override
    public void removeStaticContent( String contentLocation, String serverMountPoint )
    {
        staticContent.remove( serverMountPoint );
    }

    @Override
    public void invokeDirectly( String targetPath, HttpServletRequest request, HttpServletResponse response )
            throws IOException, ServletException
    {
        jetty.handle( targetPath, (Request) request, request, response );
    }

    @Override
    public void setRequestLog( RequestLog requestLog )
    {
        this.requestLog = requestLog;
    }

    @Override
    public void setHttpsAddress( Optional address )
    {
        jettyHttpsAddress = address;
    }

    @Override
    public void setHttpsCertificateInformation( KeyStoreInformation config )
    {
        httpsCertificateInformation = config;
    }

    public Server getJetty()
    {
        return jetty;
    }

    protected void startJetty() throws Exception
    {
        try
        {
            jetty.start();
        }
        catch( BindException e )
        {
            throw new PortBindException( jettyAddress, e );
        }
    }

    private void loadAllMounts() throws IOException
    {
        SessionManager sm = new HashSessionManager();

        final SortedSet mountpoints = new TreeSet<>( (Comparator) ( o1, o2 ) -> o2.compareTo( o1 ) );

        mountpoints.addAll( staticContent.keySet() );
        mountpoints.addAll( jaxRSPackages.keySet() );
        mountpoints.addAll( jaxRSClasses.keySet() );

        for ( String contentKey : mountpoints )
        {
            final boolean isStatic = staticContent.containsKey( contentKey );
            final boolean isJaxrsPackage = jaxRSPackages.containsKey( contentKey );
            final boolean isJaxrsClass = jaxRSClasses.containsKey( contentKey );

            if ( countSet( isStatic, isJaxrsPackage, isJaxrsClass ) > 1 )
            {
                throw new RuntimeException(
                        format( "content-key '%s' is mapped more than once", contentKey ) );
            }
            else if ( isStatic )
            {
                loadStaticContent( sm, contentKey );
            }
            else if ( isJaxrsPackage )
            {
                loadJAXRSPackage( sm, contentKey );
            }
            else if ( isJaxrsClass )
            {
                loadJAXRSClasses( sm, contentKey );
            }
            else
            {
                throw new RuntimeException( format( "content-key '%s' is not mapped", contentKey ) );
            }
        }
    }

    private int countSet( boolean... booleans )
    {
        int count = 0;
        for ( boolean bool : booleans )
        {
            if ( bool )
            {
                count++;
            }
        }
        return count;
    }

    private void loadRequestLogging()
    {
        // This makes the request log handler decorate whatever other handlers are already set up
        final RequestLogHandler requestLogHandler = new RequestLogHandler();
        requestLogHandler.setRequestLog( requestLog );
        requestLogHandler.setServer( jetty );
        requestLogHandler.setHandler( jetty.getHandler() );
        jetty.setHandler( requestLogHandler );
    }

    private String trimTrailingSlashToKeepJettyHappy( String mountPoint )
    {
        if ( mountPoint.equals( "/" ) )
        {
            return mountPoint;
        }

        if ( mountPoint.endsWith( "/" ) )
        {
            mountPoint = mountPoint.substring( 0, mountPoint.length() - 1 );
        }
        return mountPoint;
    }

    private String ensureRelativeUri( String mountPoint )
    {
        try
        {
            URI result = new URI( mountPoint );
            if ( result.isAbsolute() )
            {
                return result.getPath();
            }
            else
            {
                return result.toString();
            }
        }
        catch ( URISyntaxException e )
        {
            log.debug( "Unable to translate [%s] to a relative URI in ensureRelativeUri(String mountPoint)", mountPoint );
            return mountPoint;
        }
    }

    private void loadStaticContent( SessionManager sm, String mountPoint )
    {
        String contentLocation = staticContent.get( mountPoint );
        try
        {
            SessionHandler sessionHandler = new SessionHandler( sm );
            sessionHandler.setServer( getJetty() );
            final WebAppContext staticContext = new WebAppContext();
            staticContext.setServer( getJetty() );
            staticContext.setContextPath( mountPoint );
            staticContext.setSessionHandler( sessionHandler );
            staticContext.setInitParameter( "org.eclipse.jetty.servlet.Default.dirAllowed", "false" );
            URL resourceLoc = getClass().getClassLoader().getResource( contentLocation );
            if ( resourceLoc != null )
            {
                URL url = resourceLoc.toURI().toURL();
                final Resource resource = Resource.newResource( url );
                staticContext.setBaseResource( resource );

                addFiltersTo( staticContext );
                staticContext.addFilter( new FilterHolder( new NoCacheHtmlFilter() ), "/*",
                        EnumSet.of( DispatcherType.REQUEST, DispatcherType.FORWARD ) );

                handlers.addHandler( staticContext );
            }
            else
            {
                log.warn( "No static content available for Neo Server at %s, management console may not be available.",
                        jettyAddress );
            }
        }
        catch ( Exception e )
        {
            log.error( "Unknown error loading static content", e );
            e.printStackTrace();
            throw new RuntimeException( e );
        }
    }

    private void loadJAXRSPackage( SessionManager sm, String mountPoint )
    {
        loadJAXRSResource( sm, mountPoint, jaxRSPackages.get( mountPoint ) );
    }

    private void loadJAXRSClasses( SessionManager sm, String mountPoint )
    {
        loadJAXRSResource( sm, mountPoint, jaxRSClasses.get( mountPoint ) );
    }

    private void loadJAXRSResource( SessionManager sm, String mountPoint,
                                    JaxRsServletHolderFactory jaxRsServletHolderFactory )
    {
        SessionHandler sessionHandler = new SessionHandler( sm );
        sessionHandler.setServer( getJetty() );
        log.debug( "Mounting servlet at [%s]", mountPoint );
        ServletContextHandler jerseyContext = new ServletContextHandler();
        jerseyContext.setServer( getJetty() );
        jerseyContext.setErrorHandler( new NeoJettyErrorHandler() );
        jerseyContext.setContextPath( mountPoint );
        jerseyContext.setSessionHandler( sessionHandler );
        jerseyContext.addServlet( jaxRsServletHolderFactory.create( defaultInjectables, wadlEnabled ), "/*" );
        addFiltersTo( jerseyContext );
        handlers.addHandler( jerseyContext );
    }

    private void addFiltersTo( ServletContextHandler context )
    {
        for ( FilterDefinition filterDef : filters )
        {
            context.addFilter( new FilterHolder( filterDef.getFilter() ),
                    filterDef.getPathSpec(), EnumSet.allOf( DispatcherType.class )
            );
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy