blob: 39838f9ed841c388ec34fad0870e3cef719e7dae [file] [log] [blame]
/**
* 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.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.jena.atlas.lib.FileOps;
import org.apache.jena.atlas.web.WebLib;
import org.apache.jena.fuseki.cmd.FusekiArgs;
import org.apache.jena.fuseki.cmd.JettyFusekiWebapp;
import org.apache.jena.fuseki.cmd.JettyServerConfig;
import org.apache.jena.fuseki.webapp.FusekiEnv;
import org.apache.jena.fuseki.webapp.FusekiServerListener;
import org.apache.jena.fuseki.webapp.FusekiWebapp;
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.update.Update;
import org.apache.jena.update.UpdateExecutionFactory;
import org.apache.jena.update.UpdateProcessor;
/**
* <b>Note:</b>
* <br/>
* <em> There is a {@code FusekiTestServer} in the basic Fuseki server which is more
* appropriate for testing SPARQL protocols. It does not have an on-disk footprint.</em>
* <br/>
* This class is
* primarily for testing the full Fuseki server and has a full on-disk configuration.
*
* 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 reset between tests.
* <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(); }
*/
// 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 = WebLib.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 datasetName() { return "dataset"; }
public static final String datasetPath() { return "/"+datasetName(); }
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 ) {
setHttpClient();
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 ) {
setHttpClient();
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 ) {
setHttpClient();
allocServer();
}
}
/**
* Clean up after each test by resetting the Fuseki dataset
*/
public static void ctlAfterTest() {
if ( serverScope == TEST ) {
freeServer();
resetDefaultHttpClient();
} else
resetServer();
}
private static void setHttpClient() {}
private static void resetDefaultHttpClient() {}
// reference count of start/stop server
private static AtomicInteger countServer = new AtomicInteger();
private static JettyFusekiWebapp 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) {
// Does not initial Fuseki webapp.
FusekiEnv.FUSEKI_HOME = Path.of(TS_FusekiWebapp.FusekiTestHome).toAbsolutePath();
FileOps.ensureDir("target");
FileOps.ensureDir(TS_FusekiWebapp.FusekiTestHome);
FileOps.ensureDir(TS_FusekiWebapp.FusekiTestBase);
FusekiEnv.FUSEKI_BASE = Path.of(TS_FusekiWebapp.FusekiTestBase).toAbsolutePath();
// Must have shiro.ini.
// This fakes the state after FusekiSystem initialization
// in the case of starting in the same location. FusekiSystem has statics.
// Fuseki-full is designed to be the only server, not restartable.
// Here, we want to reset for testing.
FusekiWebapp.formatBaseArea();
emptyDirectory(FusekiWebapp.dirSystemDatabase);
emptyDirectory(FusekiWebapp.dirBackups);
emptyDirectory(FusekiWebapp.dirLogs);
emptyDirectory(FusekiWebapp.dirConfiguration);
emptyDirectory(FusekiWebapp.dirDatabases);
emptyDirectory(FusekiWebapp.dirSystemFileArea);
setupServer(port(), null, datasetPath(), updateable);
}
private static void emptyDirectory(Path directory) {
if ( directory == null )
// Server will create.
return;
FileOps.clearAll(directory.toString());
}
public static void setupServer(int port, String authConfigFile, String datasetPath, boolean updateable) {
FusekiArgs params = new FusekiArgs();
dsgTesting = DatasetGraphFactory.createTxnMem();
params.dsg = dsgTesting;
params.datasetPath = datasetPath;
params.allowUpdate = updateable;
FusekiServerListener.initialSetup = params;
JettyServerConfig config = make(port, true);
config.authConfigFile = authConfigFile;
JettyFusekiWebapp.initializeServer(config);
JettyFusekiWebapp.instance.start();
server = JettyFusekiWebapp.instance;
}
/*package*/ static void teardownServer() {
if ( server != null ) {
// Clear out the registry.
server.getDataAccessPointRegistry().clear();
FileOps.clearAll(FusekiWebapp.dirConfiguration.toFile());
server.stop();
}
server = null;
}
/*package*/ static JettyServerConfig make(int port, boolean allowUpdate) {
JettyServerConfig config = new JettyServerConfig();
// Avoid any persistent record.
config.port = port;
config.contextPath = "/";
// Always execute tests with localhost only access.
config.loopback = true;
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;}
}
}
}