/**
* 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.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.fuseki.server.DatasetRegistry ;
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.update.Update ;
import org.apache.jena.update.UpdateExecutionFactory ;
import org.apache.jena.update.UpdateProcessor ;
/**
* Manage a single server for use wth 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 import 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 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 */
private static void setHttpClient(HttpClient newHttpClient) {
HttpClient hc = HttpOp.getDefaultHttpClient() ;
if ( hc instanceof CloseableHttpClient )
IO.close((CloseableHttpClient)hc) ;
HttpOp.setDefaultHttpClient(newHttpClient) ;
}
@SuppressWarnings("deprecation")
private static EmbeddedFusekiServer1 server = null ;
// reference count of start/stop server
private static AtomicInteger countServer = new AtomicInteger() ;
/*package*/ static void allocServer() {
if ( countServer.getAndIncrement() == 0 )
setupServer() ;
}
/*package*/ static void freeServer() {
if ( countServer.decrementAndGet() == 0 )
teardownServer() ;
}
@SuppressWarnings("deprecation")
protected static void setupServer() {
dsgTesting = DatasetGraphFactory.createTxnMem() ;
server = EmbeddedFusekiServer1.create(currentPort, dsgTesting, datasetPath()) ;
server.start() ;
}
@SuppressWarnings("deprecation")
protected static void teardownServer() {
DatasetRegistry.get().clear() ;
if ( server != null )
server.stop() ;
server = null ;
}
/*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!");
}
}
}