/**
* 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.embedded;
import static org.apache.jena.fuseki.embedded.FusekiTestServer.ServerScope.CLASS ;
import static org.apache.jena.fuseki.embedded.FusekiTestServer.ServerScope.SUITE ;
import static org.apache.jena.fuseki.embedded.FusekiTestServer.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.FusekiException ;
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 ;
// NOT FINISHED
/**
* Manage a single server for use with tests. It supports three modes:
* <ul>
* <li>{@code ServerScope.SUITE} : One server for a whole test suite
* <li>{@code ServerScope.CLASS} : One server per test class
* <li>{@code ServerScope.TEST} :One server per individual test
* </ul>
* One server per individual test can be troublesome due to connections not closing down
* fast enough and can also be slow.
* <p> One server per test class is a good compromise.
* <p> The data in the server is always reset between tests in all modes.
* <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 FusekiTestServer {
/* 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 ;
// reference count of start/stop server
private static AtomicInteger countServer = new AtomicInteger() ;
private static FusekiEmbeddedServer server = null ;
public static final String urlRoot() { return "http://localhost:"+port()+"/" ; }
public static final String datasetPath() { return "/ds_test" ; }
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) ;
}
/*package*/ static void allocServer() {
if ( countServer.getAndIncrement() == 0 )
setupServer(true) ;
}
/*package*/ static void freeServer() {
if ( countServer.decrementAndGet() == 0 )
teardownServer() ;
}
/*package*/ static void setupServer(boolean updateable) {
dsgTesting = DatasetGraphFactory.createTxnMem() ;
server =
FusekiEmbeddedServer.create()
.setPort(port())
.setLoopback(true)
.add(datasetPath(), dsgTesting)
.build();
}
/*package*/ static void teardownServer() {
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!");
}
}
}