/* $Id$ */
/**
* 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.manifoldcf.crawler.tests;
import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.agents.interfaces.*;
import org.apache.manifoldcf.crawler.interfaces.*;
import org.apache.manifoldcf.crawler.system.ManifoldCF;
import org.apache.manifoldcf.agents.system.AgentsDaemon;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.nio.charset.Charset;
import org.junit.*;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.webapp.WebAppContext;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.config.SocketConfig;
import org.apache.http.HttpStatus;
import org.apache.http.HttpException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpEntity;
import org.apache.http.util.EntityUtils;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.ParseException;
/** Tests that run the "agents daemon" should be derived from this */
public class ManifoldCFInstance
{
protected final boolean webapps;
protected final boolean singleWar;
protected final int testPort;
protected final String processID;
protected DaemonThread daemonThread = null;
protected Server server = null;
public ManifoldCFInstance()
{
this("", 8346, false, true);
}
public ManifoldCFInstance(String processID)
{
this(processID, 8346, false, true);
}
public ManifoldCFInstance(boolean singleWar)
{
this("", 8346, singleWar, true);
}
public ManifoldCFInstance(String processID, boolean singleWar)
{
this(processID, 8346, singleWar, true);
}
public ManifoldCFInstance(boolean singleWar, boolean webapps)
{
this("", 8346, singleWar, webapps);
}
public ManifoldCFInstance(String processID, boolean singleWar, boolean webapps)
{
this(processID, 8346, singleWar, webapps);
}
public ManifoldCFInstance(String processID, int testPort)
{
this(processID, testPort, false, true);
}
public ManifoldCFInstance(String processID, int testPort, boolean singleWar)
{
this(processID, testPort, singleWar, true);
}
public ManifoldCFInstance(String processID, int testPort, boolean singleWar, boolean webapps)
{
this.processID = processID;
this.webapps = webapps;
this.testPort = testPort;
this.singleWar = singleWar;
}
// Basic job support
public void waitJobInactiveNative(IJobManager jobManager, Long jobID, long maxTime)
throws ManifoldCFException, InterruptedException
{
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() < startTime + maxTime)
{
JobStatus status = jobManager.getStatus(jobID);
if (status == null)
throw new ManifoldCFException("No such job: '"+jobID+"'");
int statusValue = status.getStatus();
switch (statusValue)
{
case JobStatus.JOBSTATUS_NOTYETRUN:
throw new ManifoldCFException("Job was never started.");
case JobStatus.JOBSTATUS_COMPLETED:
break;
case JobStatus.JOBSTATUS_ERROR:
throw new ManifoldCFException("Job reports error status: "+status.getErrorText());
default:
ManifoldCF.sleep(1000L);
continue;
}
return;
}
throw new ManifoldCFException("ManifoldCF did not terminate in the allotted time of "+new Long(maxTime).toString()+" milliseconds");
}
public void waitJobDeletedNative(IJobManager jobManager, Long jobID, long maxTime)
throws ManifoldCFException, InterruptedException
{
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() < startTime + maxTime)
{
JobStatus status = jobManager.getStatus(jobID);
if (status == null)
return;
ManifoldCF.sleep(1000L);
}
throw new ManifoldCFException("ManifoldCF did not delete in the allotted time of "+new Long(maxTime).toString()+" milliseconds");
}
public void waitJobRunningNative(IJobManager jobManager, Long jobID, long maxTime)
throws ManifoldCFException, InterruptedException
{
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() < startTime + maxTime)
{
JobStatus status = jobManager.getStatus(jobID);
if (status == null)
throw new ManifoldCFException("No such job: '"+jobID+"'");
int statusValue = status.getStatus();
switch (statusValue)
{
case JobStatus.JOBSTATUS_NOTYETRUN:
throw new ManifoldCFException("Job was never started.");
case JobStatus.JOBSTATUS_COMPLETED:
throw new ManifoldCFException("Job ended on its own!");
case JobStatus.JOBSTATUS_ERROR:
throw new ManifoldCFException("Job reports error status: "+status.getErrorText());
case JobStatus.JOBSTATUS_RUNNING:
break;
default:
ManifoldCF.sleep(1000L);
continue;
}
return;
}
throw new ManifoldCFException("ManifoldCF did not start in the allotted time of "+new Long(maxTime).toString()+" milliseconds");
}
// API support
// These methods allow communication with the ManifoldCF api webapp, via the locally-instantiated jetty
public void loginAPI(String userID, String password)
throws Exception
{
Configuration requestObject = new Configuration();
ConfigurationNode cn = new ConfigurationNode("userID");
cn.setValue(userID);
requestObject.addChild(requestObject.getChildCount(),cn);
cn = new ConfigurationNode("password");
cn.setValue(password);
requestObject.addChild(requestObject.getChildCount(),cn);
performAPIPostOperationViaNodes("LOGIN",200,requestObject);
}
public void startJobAPI(String jobIDString)
throws Exception
{
Configuration requestObject = new Configuration();
Configuration result = performAPIPutOperationViaNodes("start/"+jobIDString,201,requestObject);
int i = 0;
while (i < result.getChildCount())
{
ConfigurationNode resultNode = result.findChild(i++);
if (resultNode.getType().equals("error"))
throw new Exception(resultNode.getValue());
}
}
public void deleteJobAPI(String jobIDString)
throws Exception
{
Configuration result = performAPIDeleteOperationViaNodes("jobs/"+jobIDString,200);
int i = 0;
while (i < result.getChildCount())
{
ConfigurationNode resultNode = result.findChild(i++);
if (resultNode.getType().equals("error"))
throw new Exception(resultNode.getValue());
}
}
public String getJobStatusAPI(String jobIDString)
throws Exception
{
Configuration result = performAPIGetOperationViaNodes("jobstatusesnocounts/"+jobIDString,200);
String status = null;
int i = 0;
while (i < result.getChildCount())
{
ConfigurationNode resultNode = result.findChild(i++);
if (resultNode.getType().equals("error"))
throw new Exception(resultNode.getValue());
else if (resultNode.getType().equals("jobstatus"))
{
int j = 0;
while (j < resultNode.getChildCount())
{
ConfigurationNode childNode = resultNode.findChild(j++);
if (childNode.getType().equals("status"))
status = childNode.getValue();
}
}
}
return status;
}
public long getJobDocumentsProcessedAPI(String jobIDString)
throws Exception
{
Configuration result = performAPIGetOperationViaNodes("jobstatuses/"+jobIDString,200);
String documentsProcessed = null;
int i = 0;
while (i < result.getChildCount())
{
ConfigurationNode resultNode = result.findChild(i++);
if (resultNode.getType().equals("error"))
throw new Exception(resultNode.getValue());
else if (resultNode.getType().equals("jobstatus"))
{
int j = 0;
while (j < resultNode.getChildCount())
{
ConfigurationNode childNode = resultNode.findChild(j++);
if (childNode.getType().equals("documents_processed"))
documentsProcessed = childNode.getValue();
}
}
}
if (documentsProcessed == null)
throw new Exception("Expected a documents_processed field, didn't find it");
return new Long(documentsProcessed).longValue();
}
public void waitJobInactiveAPI(String jobIDString, long maxTime)
throws Exception
{
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() < startTime + maxTime)
{
String status = getJobStatusAPI(jobIDString);
if (status == null)
throw new Exception("No such job: '"+jobIDString+"'");
if (status.equals("not yet run"))
throw new Exception("Job was never started.");
if (status.equals("done"))
return;
if (status.equals("error"))
throw new Exception("Job reports error.");
ManifoldCF.sleep(1000L);
continue;
}
throw new ManifoldCFException("ManifoldCF did not terminate in the allotted time of "+new Long(maxTime).toString()+" milliseconds");
}
public void waitJobDeletedAPI(String jobIDString, long maxTime)
throws Exception
{
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() < startTime + maxTime)
{
String status = getJobStatusAPI(jobIDString);
if (status == null)
return;
ManifoldCF.sleep(1000L);
}
throw new ManifoldCFException("ManifoldCF did not delete in the allotted time of "+new Long(maxTime).toString()+" milliseconds");
}
/** Construct a command url.
*/
public String makeAPIURL(String command)
throws Exception
{
if (webapps)
{
if (singleWar)
return "http://localhost:"+Integer.toString(testPort)+"/mcf/api/json/"+command;
else
return "http://localhost:"+Integer.toString(testPort)+"/mcf-api-service/json/"+command;
}
else
throw new Exception("No API servlet running");
}
public static String convertToString(HttpResponse httpResponse)
throws IOException
{
HttpEntity entity = httpResponse.getEntity();
if (entity != null)
{
InputStream is = entity.getContent();
try
{
Charset charSet;
try
{
ContentType ct = ContentType.get(entity);
if (ct == null)
charSet = StandardCharsets.UTF_8;
else
charSet = ct.getCharset();
}
catch (ParseException e)
{
charSet = StandardCharsets.UTF_8;
}
char[] buffer = new char[65536];
Reader r = new InputStreamReader(is,charSet);
Writer w = new StringWriter();
try
{
while (true)
{
int amt = r.read(buffer);
if (amt == -1)
break;
w.write(buffer,0,amt);
}
}
finally
{
w.flush();
}
return w.toString();
}
finally
{
is.close();
}
}
return "";
}
/** Perform an json API GET operation.
*@param apiURL is the operation.
*@param expectedResponse is the expected response code.
*@return the json response.
*/
public String performAPIGetOperation(String apiURL, int expectedResponse)
throws Exception
{
HttpClient client = HttpClients.createDefault();
HttpGet method = new HttpGet(apiURL);
try
{
HttpResponse response = client.execute(method);
int responseCode = response.getStatusLine().getStatusCode();
String responseString = convertToString(response);
if (responseCode != expectedResponse)
throw new Exception("API http error; expected "+Integer.toString(expectedResponse)+", saw "+Integer.toString(responseCode)+": "+responseString);
return responseString;
}
finally
{
method.abort();
}
}
/** Perform an json API DELETE operation.
*@param apiURL is the operation.
*@param expectedResponse is the expected response code.
*@return the json response.
*/
public String performAPIDeleteOperation(String apiURL, int expectedResponse)
throws Exception
{
HttpClient client = HttpClients.createDefault();
HttpDelete method = new HttpDelete(apiURL);
try
{
HttpResponse response = client.execute(method);
int responseCode = response.getStatusLine().getStatusCode();
String responseString = convertToString(response);
if (responseCode != expectedResponse)
throw new Exception("API http error; expected "+Integer.toString(expectedResponse)+", saw "+Integer.toString(responseCode)+": "+responseString);
// We presume that the data is utf-8, since that's what the API uses throughout.
return responseString;
}
finally
{
method.abort();
}
}
/** Perform an json API PUT operation.
*@param apiURL is the operation.
*@param input is the input JSON.
*@param expectedResponse is the expected response code.
*@return the json response.
*/
public String performAPIPutOperation(String apiURL, int expectedResponse, String input)
throws Exception
{
HttpClient client = HttpClients.createDefault();
HttpPut method = new HttpPut(apiURL);
try
{
method.setEntity(new StringEntity(input,ContentType.create("text/plain",StandardCharsets.UTF_8)));
HttpResponse response = client.execute(method);
int responseCode = response.getStatusLine().getStatusCode();
String responseString = convertToString(response);
if (responseCode != expectedResponse)
throw new Exception("API http error; expected "+Integer.toString(expectedResponse)+", saw "+Integer.toString(responseCode)+": "+responseString);
// We presume that the data is utf-8, since that's what the API uses throughout.
return responseString;
}
finally
{
method.abort();
}
}
/** Perform an json API POST operation.
*@param apiURL is the operation.
*@param input is the input JSON.
*@param expectedResponse is the expected response code.
*@return the json response.
*/
public String performAPIPostOperation(String apiURL, int expectedResponse, String input)
throws Exception
{
HttpClient client = HttpClients.createDefault();
HttpPost method = new HttpPost(apiURL);
try
{
method.setEntity(new StringEntity(input,ContentType.create("text/plain",StandardCharsets.UTF_8)));
HttpResponse response = client.execute(method);
int responseCode = response.getStatusLine().getStatusCode();
String responseString = convertToString(response);
if (responseCode != expectedResponse)
throw new Exception("API http error; expected "+Integer.toString(expectedResponse)+", saw "+Integer.toString(responseCode)+": "+responseString);
// We presume that the data is utf-8, since that's what the API uses throughout.
return responseString;
}
finally
{
method.abort();
}
}
/** Perform a json GET API operation, using Configuration structures to represent the json. This is for testing convenience,
* mostly.
*/
public Configuration performAPIGetOperationViaNodes(String command, int expectedResponse)
throws Exception
{
String result = performAPIGetOperation(makeAPIURL(command),expectedResponse);
Configuration cfg = new Configuration();
cfg.fromJSON(result);
return cfg;
}
/** Perform a json DELETE API operation, using Configuration structures to represent the json. This is for testing convenience,
* mostly.
*/
public Configuration performAPIDeleteOperationViaNodes(String command, int expectedResponse)
throws Exception
{
String result = performAPIDeleteOperation(makeAPIURL(command),expectedResponse);
Configuration cfg = new Configuration();
cfg.fromJSON(result);
return cfg;
}
/** Perform a json PUT API operation, using Configuration structures to represent the json. This is for testing convenience,
* mostly.
*/
public Configuration performAPIPutOperationViaNodes(String command, int expectedResponse, Configuration argument)
throws Exception
{
String argumentJson;
if (argument != null)
argumentJson = argument.toJSON();
else
argumentJson = null;
String result = performAPIPutOperation(makeAPIURL(command),expectedResponse,argumentJson);
Configuration cfg = new Configuration();
cfg.fromJSON(result);
return cfg;
}
/** Perform a json POST API operation, using Configuration structures to represent the json. This is for testing convenience,
* mostly.
*/
public Configuration performAPIPostOperationViaNodes(String command, int expectedResponse, Configuration argument)
throws Exception
{
String argumentJson;
if (argument != null)
argumentJson = argument.toJSON();
else
argumentJson = null;
String result = performAPIPostOperation(makeAPIURL(command),expectedResponse,argumentJson);
Configuration cfg = new Configuration();
cfg.fromJSON(result);
return cfg;
}
// Setup/teardown
public void start()
throws Exception
{
ContextHandlerCollection contexts = new ContextHandlerCollection();
if (webapps)
{
// Start jetty
server = new Server( testPort );
server.setStopAtShutdown( true );
// Initialize the servlets
server.setHandler(contexts);
}
if (singleWar)
{
if (webapps)
{
// Start the single combined war
String combinedWarPath = "../../framework/build/war-proprietary/mcf-combined-service.war";
if (System.getProperty("combinedWarPath") != null)
combinedWarPath = System.getProperty("combinedWarPath");
// Initialize the servlet
WebAppContext lcfCombined = new WebAppContext(combinedWarPath,"/mcf");
// This will cause jetty to ignore all of the framework and jdbc jars in the war, which is what we want.
lcfCombined.setParentLoaderPriority(true);
contexts.addHandler(lcfCombined);
server.start();
}
else
throw new Exception("Can't run singleWar without webapps");
}
else
{
if (webapps)
{
String crawlerWarPath = "../../framework/build/war-proprietary/mcf-crawler-ui.war";
String authorityserviceWarPath = "../../framework/build/war-proprietary/mcf-authority-service.war";
String apiWarPath = "../../framework/build/war-proprietary/mcf-api-service.war";
if (System.getProperty("crawlerWarPath") != null)
crawlerWarPath = System.getProperty("crawlerWarPath");
if (System.getProperty("authorityserviceWarPath") != null)
authorityserviceWarPath = System.getProperty("authorityserviceWarPath");
if (System.getProperty("apiWarPath") != null)
apiWarPath = System.getProperty("apiWarPath");
// Initialize the servlets
WebAppContext lcfCrawlerUI = new WebAppContext(crawlerWarPath,"/mcf-crawler-ui");
// This will cause jetty to ignore all of the framework and jdbc jars in the war, which is what we want.
lcfCrawlerUI.setParentLoaderPriority(true);
contexts.addHandler(lcfCrawlerUI);
WebAppContext lcfAuthorityService = new WebAppContext(authorityserviceWarPath,"/mcf-authority-service");
// This will cause jetty to ignore all of the framework and jdbc jars in the war, which is what we want.
lcfAuthorityService.setParentLoaderPriority(true);
contexts.addHandler(lcfAuthorityService);
WebAppContext lcfApi = new WebAppContext(apiWarPath,"/mcf-api-service");
lcfApi.setParentLoaderPriority(true);
contexts.addHandler(lcfApi);
server.start();
}
// If all worked, then we can start the daemon.
// Clear the agents shutdown signal.
IThreadContext tc = ThreadContextFactory.make();
AgentsDaemon.clearAgentsShutdownSignal(tc);
daemonThread = new DaemonThread(processID);
daemonThread.start();
}
}
public void stop()
throws Exception
{
Exception currentException = null;
IThreadContext tc = ThreadContextFactory.make();
// Delete all jobs (and wait for them to go away)
if (daemonThread != null || singleWar)
{
IJobManager jobManager = JobManagerFactory.make(tc);
// Get a list of the current active jobs
IJobDescription[] jobs = jobManager.getAllJobs();
int i = 0;
while (i < jobs.length)
{
IJobDescription desc = jobs[i++];
// Abort this job, if it is running
try
{
jobManager.manualAbort(desc.getID());
}
catch (ManifoldCFException e)
{
// This generally means that the job was not running
}
}
i = 0;
while (i < jobs.length)
{
IJobDescription desc = jobs[i++];
// Wait for this job to stop
while (true)
{
JobStatus status = jobManager.getStatus(desc.getID(),false);
if (status != null)
{
int statusValue = status.getStatus();
switch (statusValue)
{
case JobStatus.JOBSTATUS_NOTYETRUN:
case JobStatus.JOBSTATUS_COMPLETED:
case JobStatus.JOBSTATUS_ERROR:
break;
default:
ManifoldCF.sleep(10000);
continue;
}
}
break;
}
}
// Now, delete them all
i = 0;
while (i < jobs.length)
{
IJobDescription desc = jobs[i++];
try
{
jobManager.deleteJob(desc.getID());
}
catch (ManifoldCFException e)
{
// This usually means that the job is already being deleted
}
}
i = 0;
while (i < jobs.length)
{
IJobDescription desc = jobs[i++];
// Wait for this job to disappear
while (true)
{
JobStatus status = jobManager.getStatus(desc.getID(),false);
if (status != null)
{
ManifoldCF.sleep(10000);
continue;
}
break;
}
}
try
{
stopNoCleanup();
}
catch (Exception e)
{
if (currentException == null)
currentException = e;
}
}
}
public void stopNoCleanup()
throws Exception
{
if (daemonThread != null)
{
Exception currentException = null;
// Shut down daemon - but only ONE daemon
//AgentsDaemon.assertAgentsShutdownSignal(tc);
// Wait for daemon thread to exit.
while (true)
{
daemonThread.interrupt();
if (daemonThread.isAlive())
{
Thread.sleep(1000L);
continue;
}
break;
}
Exception e = daemonThread.getDaemonException();
if (e != null || !(e instanceof InterruptedException))
currentException = e;
if (currentException != null)
throw currentException;
}
}
public void unload()
throws Exception
{
if (server != null)
{
// Unfortunately, this causes the shutdown hooks to be called, which causes
// no end of trouble unless it is done last.
server.stop();
server.join();
server = null;
}
}
protected static class DaemonThread extends Thread
{
protected final String processID;
protected Exception daemonException = null;
public DaemonThread(String processID)
{
this.processID = processID;
setName("Daemon thread");
}
public void run()
{
IThreadContext tc = ThreadContextFactory.make();
// Now, start the server, and then wait for the shutdown signal. On shutdown, we have to actually do the cleanup,
// because the JVM isn't going away.
AgentsDaemon ad = new AgentsDaemon(processID);
try
{
ad.runAgents(tc);
}
catch (ManifoldCFException e)
{
daemonException = e;
}
finally
{
try
{
ad.stopAgents(tc);
}
catch (ManifoldCFException e)
{
daemonException = e;
}
}
}
public Exception getDaemonException()
{
return daemonException;
}
}
}