/** * 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; import static org.apache.jena.fuseki.ServerCtl.ServerScope.CLASS ; import static org.apache.jena.fuseki.ServerCtl.ServerScope.SUITE ; import static org.apache.jena.fuseki.ServerCtl.ServerScope.TEST ; import java.io.IOException ; import java.net.ServerSocket ; import java.nio.file.Paths ; import java.util.concurrent.atomic.AtomicInteger ; import org.apache.http.client.HttpClient ; import org.apache.http.impl.client.CloseableHttpClient ; import org.apache.jena.atlas.io.IO ; import org.apache.jena.atlas.lib.FileOps ; import org.apache.jena.fuseki.jetty.JettyFuseki ; import org.apache.jena.fuseki.jetty.JettyServerConfig ; import org.apache.jena.fuseki.server.* ; import org.apache.jena.riot.web.HttpOp ; import org.apache.jena.sparql.core.DatasetGraph ; import org.apache.jena.sparql.core.DatasetGraphFactory ; import org.apache.jena.sparql.modify.request.Target ; import org.apache.jena.sparql.modify.request.UpdateDrop ; import org.apache.jena.system.Txn ; import org.apache.jena.tdb.base.file.Location ; import org.apache.jena.update.Update ; import org.apache.jena.update.UpdateExecutionFactory ; import org.apache.jena.update.UpdateProcessor ; /** * Manage a single server for use with tests. It supports three modes: * <ul> * <li>One server for a whole test suite * <li>One server per test class * <li>One server per individual test * </ul> * One server per individual test can be troublesome due to connections not closing down * fast enough (left in TCP state {@code TIME_WAIT} which is 2 minutes) and also can be slow. * One server per test class is a good compromise. * <p> The data in the server is always reseet between tests. * <p> * Using a connection pooling HttpClient (see {@link HttpOp#createPoolingHttpClient()}) is important, * both for test performance and for reducing the TCP connection load on the operating system. * <p> * Usage: * </p> * <p> * In the test suite, put: * * <pre> * {@literal @BeforeClass} static public void beforeSuiteClass() { ServerCtl.ctlBeforeTestSuite(); } * {@literal @AfterClass} static public void afterSuiteClass() { ServerCtl.ctlAfterTestSuite(); } * </pre> * <p> * In the test class, put: * <pre> * {@literal @BeforeClass} public static void ctlBeforeClass() { ServerCtl.ctlBeforeClass(); } * {@literal @AfterClass} public static void ctlAfterClass() { ServerCtl.ctlAfterClass(); } * {@literal @Before} public void ctlBeforeTest() { ServerCtl.ctlBeforeTest(); } * {@literal @After} public void ctlAfterTest() { ServerCtl.ctlAfterTest(); } * </pre> */ public class ServerCtl { static { Fuseki.init(); } /* Cut&Paste versions: Test suite (TS_*) @BeforeClass static public void beforeSuiteClass() { ServerCtl.ctlBeforeTestSuite(); } @AfterClass static public void afterSuiteClass() { ServerCtl.ctlAfterTestSuite(); } Test class (Test*) @BeforeClass public static void ctlBeforeClass() { ServerCtl.ctlBeforeClass(); } @AfterClass public static void ctlAfterClass() { ServerCtl.ctlAfterClass(); } @Before public void ctlBeforeTest() { ServerCtl.ctlBeforeTest(); } @After public void ctlAfterTest() { ServerCtl.ctlAfterTest(); } */ static HttpClient defaultHttpClient = HttpOp.getDefaultHttpClient(); // Note: it is important to cleanly close a PoolingHttpClient across server restarts // otherwise the pooled connections remain for the old server. /*package : for import static */ enum ServerScope { SUITE, CLASS, TEST } private static ServerScope serverScope = ServerScope.CLASS ; private static int currentPort = choosePort() ; public static int port() { return currentPort ; } // Whether to use a transaction on the dataset or to use SPARQL Update. static boolean CLEAR_DSG_DIRECTLY = true ; static private DatasetGraph dsgTesting ; // Abstraction that runs a SPARQL server for tests. public static final String urlRoot() { return "http://localhost:"+port()+"/" ; } public static final String datasetPath() { return "/dataset" ; } public static final String urlDataset() { return "http://localhost:"+port()+datasetPath() ; } public static final String serviceUpdate() { return "http://localhost:"+port()+datasetPath()+"/update" ; } public static final String serviceQuery() { return "http://localhost:"+port()+datasetPath()+"/query" ; } public static final String serviceGSP() { return "http://localhost:"+port()+datasetPath()+"/data" ; } public static void ctlBeforeTestSuite() { if ( serverScope == SUITE ) { setPoolingHttpClient() ; allocServer(); } } public static void ctlAfterTestSuite() { if ( serverScope == SUITE ) { freeServer(); resetDefaultHttpClient() ; } } /** * Setup for the tests by allocating a Fuseki instance to work with */ public static void ctlBeforeClass() { if ( serverScope == CLASS ) { setPoolingHttpClient() ; allocServer(); } } /** * Clean up after tests by de-allocating the Fuseki instance */ public static void ctlAfterClass() { if ( serverScope == CLASS ) { freeServer(); resetDefaultHttpClient() ; } } /** * Placeholder. */ public static void ctlBeforeTest() { if ( serverScope == TEST ) { setPoolingHttpClient() ; allocServer(); } } /** * Clean up after each test by resetting the Fuseki dataset */ public static void ctlAfterTest() { if ( serverScope == TEST ) { freeServer(); resetDefaultHttpClient() ; } else resetServer(); } /** Set a PoolingHttpClient */ private static void setPoolingHttpClient() { setHttpClient(HttpOp.createPoolingHttpClient()) ; } /** Restore the original setup */ private static void resetDefaultHttpClient() { setHttpClient(defaultHttpClient); } /** Set the HttpClient - close the old one if appropriate */ /*package*/ static void setHttpClient(HttpClient newHttpClient) { HttpClient hc = HttpOp.getDefaultHttpClient() ; if ( hc instanceof CloseableHttpClient ) IO.close((CloseableHttpClient)hc) ; HttpOp.setDefaultHttpClient(newHttpClient) ; } // reference count of start/stop server private static AtomicInteger countServer = new AtomicInteger() ; private static JettyFuseki server = null ; /*package*/ static void allocServer() { if ( countServer.getAndIncrement() == 0 ) setupServer(true) ; } /*package*/ static void freeServer() { if ( countServer.decrementAndGet() == 0 ) teardownServer() ; } protected static void setupServer(boolean updateable) { FusekiEnv.FUSEKI_HOME = Paths.get(TS_Fuseki.FusekiTestHome).toAbsolutePath() ; FileOps.ensureDir("target"); FileOps.ensureDir(TS_Fuseki.FusekiTestHome); FileOps.ensureDir(TS_Fuseki.FusekiTestBase) ; FusekiEnv.FUSEKI_BASE = Paths.get(TS_Fuseki.FusekiTestBase).toAbsolutePath() ; setupServer(port(), null, datasetPath(), updateable) ; } public static void setupServer(int port, String authConfigFile, String datasetPath, boolean updateable) { SystemState.location = Location.mem() ; SystemState.init$() ; ServerInitialConfig params = new ServerInitialConfig() ; dsgTesting = DatasetGraphFactory.createTxnMem() ; params.dsg = dsgTesting ; params.datasetPath = datasetPath ; params.allowUpdate = updateable ; FusekiServerListener.initialSetup = params ; JettyServerConfig config = make(port, true, true) ; config.authConfigFile = authConfigFile ; JettyFuseki.initializeServer(config); JettyFuseki.instance.start() ; server = JettyFuseki.instance ; } /*package*/ static void teardownServer() { if ( server != null ) { // Clear out the registry. server.getDataAccessPointRegistry().clear() ; FileOps.clearAll(FusekiServer.dirConfiguration.toFile()) ; server.stop() ; } server = null ; } /*package*/ static JettyServerConfig make(int port, boolean allowUpdate, boolean listenLocal) { JettyServerConfig config = new JettyServerConfig() ; // Avoid any persistent record. config.port = port ; config.contextPath = "/" ; config.loopback = listenLocal ; config.jettyConfigFile = null ; config.enableCompression = true ; config.verboseLogging = false ; return config ; } /*package*/ static void resetServer() { if (countServer.get() == 0) throw new RuntimeException("No server started!"); if ( CLEAR_DSG_DIRECTLY ) { Txn.executeWrite(dsgTesting, ()->dsgTesting.clear()) ; } else { Update clearRequest = new UpdateDrop(Target.ALL) ; UpdateProcessor proc = UpdateExecutionFactory.createRemote(clearRequest, serviceUpdate()) ; try {proc.execute() ; } catch (Throwable e) {e.printStackTrace(); throw e;} } } /** Choose an unused port for a server to listen on */ public static int choosePort() { try (ServerSocket s = new ServerSocket(0)) { return s.getLocalPort(); } catch (IOException ex) { throw new FusekiException("Failed to find a port for tests!"); } } }