package org.activiti.rest.api.jpa;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import junit.framework.AssertionFailedError;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.FormService;
import org.activiti.engine.HistoryService;
import org.activiti.engine.IdentityService;
import org.activiti.engine.ManagementService;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.identity.Group;
import org.activiti.engine.identity.User;
import org.activiti.engine.impl.ProcessEngineImpl;
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.activiti.engine.impl.db.DbSqlSession;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.interceptor.CommandExecutor;
import org.activiti.engine.impl.jobexecutor.JobExecutor;
import org.activiti.engine.impl.test.PvmTestCase;
import org.activiti.engine.impl.test.TestHelper;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.rest.JPAWebConfigurer;
import org.activiti.rest.api.jpa.repository.MessageRepository;
import org.activiti.rest.conf.JPAApplicationConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.session.HashSessionIdManager;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
public class BaseJPARestTestCase extends PvmTestCase {
private static Logger log = LoggerFactory.getLogger(BaseJPARestTestCase.class);
protected static final int HTTP_SERVER_PORT = 7878;
protected static final String SERVER_URL_PREFIX = "http://localhost:7878/service/";
protected static final List<String> TABLENAMES_EXCLUDED_FROM_DB_CLEAN_CHECK = Arrays.asList(
"ACT_GE_PROPERTY"
);
protected static Server server;
protected static ApplicationContext appContext;
protected ObjectMapper objectMapper = new ObjectMapper();
protected static ProcessEngine processEngine;
protected String deploymentId;
protected Throwable exception;
protected static ProcessEngineConfigurationImpl processEngineConfiguration;
protected static RepositoryService repositoryService;
protected static RuntimeService runtimeService;
protected static TaskService taskService;
protected static FormService formService;
protected static HistoryService historyService;
protected static IdentityService identityService;
protected static ManagementService managementService;
protected static MessageRepository messageRepository;
protected ISO8601DateFormat dateFormat = new ISO8601DateFormat();
static {
createAndStartServer();
// Lookup services
processEngine = appContext.getBean("processEngine", ProcessEngine.class);
processEngineConfiguration = appContext.getBean(ProcessEngineConfigurationImpl.class);
repositoryService = appContext.getBean(RepositoryService.class);
runtimeService = appContext.getBean(RuntimeService.class);
taskService = appContext.getBean(TaskService.class);
formService = appContext.getBean(FormService.class);
historyService = appContext.getBean(HistoryService.class);
identityService = appContext.getBean(IdentityService.class);
managementService = appContext.getBean(ManagementService.class);
messageRepository = appContext.getBean(MessageRepository.class);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (server != null && server.isRunning()) {
try {
server.stop();
} catch (Exception e) {
log.error("Error stopping server", e);
}
}
}
});
}
@Override
public void runBare() throws Throwable {
createUsers();
log.error(EMPTY_LINE);
try {
deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, getClass(), getName());
super.runBare();
} catch (AssertionFailedError e) {
log.error(EMPTY_LINE);
log.error("ASSERTION FAILED: {}", e, e);
exception = e;
throw e;
} catch (Throwable e) {
log.error(EMPTY_LINE);
log.error("EXCEPTION: {}", e, e);
exception = e;
throw e;
} finally {
TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, getClass(), getName());
dropUsers();
assertAndEnsureCleanDb();
processEngineConfiguration.getClock().reset();
}
}
protected void createUsers() {
User user = identityService.newUser("kermit");
user.setFirstName("Kermit");
user.setLastName("the Frog");
user.setPassword("kermit");
identityService.saveUser(user);
Group group = identityService.newGroup("admin");
group.setName("Administrators");
identityService.saveGroup(group);
identityService.createMembership(user.getId(), group.getId());
}
public static void createAndStartServer() {
server = new Server(HTTP_SERVER_PORT);
HashSessionIdManager idmanager = new HashSessionIdManager();
server.setSessionIdManager(idmanager);
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(JPAApplicationConfiguration.class);
applicationContext.refresh();
appContext = applicationContext;
try {
server.setHandler(getServletContextHandler(applicationContext));
server.start();
} catch (Exception e) {
log.error("Error starting server", e);
}
}
private static ServletContextHandler getServletContextHandler(AnnotationConfigWebApplicationContext context) throws IOException {
ServletContextHandler contextHandler = new ServletContextHandler();
JPAWebConfigurer configurer = new JPAWebConfigurer();
configurer.setContext(context);
contextHandler.addEventListener(configurer);
// Create the SessionHandler (wrapper) to handle the sessions
HashSessionManager manager = new HashSessionManager();
SessionHandler sessions = new SessionHandler(manager);
contextHandler.setHandler(sessions);
return contextHandler;
}
public HttpResponse executeHttpRequest(HttpUriRequest request, int expectedStatusCode) {
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("kermit", "kermit");
provider.setCredentials(AuthScope.ANY, credentials);
HttpClient client = HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build();
try {
if (request.getFirstHeader(HttpHeaders.CONTENT_TYPE) == null) {
// Revert to default content-type
request.addHeader(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"));
}
HttpResponse response = client.execute(request);
Assert.assertNotNull(response.getStatusLine());
Assert.assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode());
return response;
} catch (ClientProtocolException e) {
Assert.fail(e.getMessage());
} catch (IOException e) {
Assert.fail(e.getMessage());
}
return null;
}
public HttpResponse executeBinaryHttpRequest(HttpUriRequest request, int expectedStatusCode) {
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("kermit", "kermit");
provider.setCredentials(AuthScope.ANY, credentials);
HttpClient client = HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build();
try {
HttpResponse response = client.execute(request);
Assert.assertNotNull(response.getStatusLine());
Assert.assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode());
return response;
} catch (ClientProtocolException e) {
Assert.fail(e.getMessage());
} catch (IOException e) {
Assert.fail(e.getMessage());
}
return null;
}
protected void dropUsers() {
IdentityService identityService = processEngine.getIdentityService();
identityService.deleteUser("kermit");
identityService.deleteGroup("admin");
identityService.deleteMembership("kermit", "admin");
}
/** Each test is assumed to clean up all DB content it entered.
* After a test method executed, this method scans all tables to see if the DB is completely clean.
* It throws AssertionFailed in case the DB is not clean.
* If the DB is not clean, it is cleaned by performing a create a drop. */
protected void assertAndEnsureCleanDb() throws Throwable {
log.debug("verifying that db is clean after test");
Map<String, Long> tableCounts = managementService.getTableCount();
StringBuilder outputMessage = new StringBuilder();
for (String tableName : tableCounts.keySet()) {
String tableNameWithoutPrefix = tableName.replace(processEngineConfiguration.getDatabaseTablePrefix(), "");
if (!TABLENAMES_EXCLUDED_FROM_DB_CLEAN_CHECK.contains(tableNameWithoutPrefix)) {
Long count = tableCounts.get(tableName);
if (count!=0L) {
outputMessage.append(" "+tableName + ": " + count + " record(s) ");
}
}
}
if (outputMessage.length() > 0) {
outputMessage.insert(0, "DB NOT CLEAN: \n");
log.error(EMPTY_LINE);
log.error(outputMessage.toString());
log.info("dropping and recreating db");
CommandExecutor commandExecutor = ((ProcessEngineImpl)processEngine).getProcessEngineConfiguration().getCommandExecutor();
commandExecutor.execute(new Command<Object>() {
public Object execute(CommandContext commandContext) {
DbSqlSession session = commandContext.getSession(DbSqlSession.class);
session.dbSchemaDrop();
session.dbSchemaCreate();
return null;
}
});
if (exception!=null) {
throw exception;
} else {
Assert.fail(outputMessage.toString());
}
} else {
log.info("database was clean");
}
}
protected String encode(String string) {
if (string != null) {
try {
return URLEncoder.encode(string, "UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new IllegalStateException("JVM does not support UTF-8 encoding.", uee);
}
}
return null;
}
public void assertProcessEnded(final String processInstanceId) {
ProcessInstance processInstance = processEngine
.getRuntimeService()
.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
if (processInstance!=null) {
throw new AssertionFailedError("Expected finished process instance '"+processInstanceId+"' but it was still in the db");
}
}
public void waitForJobExecutorToProcessAllJobs(long maxMillisToWait, long intervalMillis) {
JobExecutor jobExecutor = processEngineConfiguration.getJobExecutor();
jobExecutor.start();
try {
Timer timer = new Timer();
InteruptTask task = new InteruptTask(Thread.currentThread());
timer.schedule(task, maxMillisToWait);
boolean areJobsAvailable = true;
try {
while (areJobsAvailable && !task.isTimeLimitExceeded()) {
Thread.sleep(intervalMillis);
areJobsAvailable = areJobsAvailable();
}
} catch (InterruptedException e) {
} finally {
timer.cancel();
}
if (areJobsAvailable) {
throw new ActivitiException("time limit of " + maxMillisToWait + " was exceeded");
}
} finally {
jobExecutor.shutdown();
}
}
public void waitForJobExecutorOnCondition(long maxMillisToWait, long intervalMillis, Callable<Boolean> condition) {
JobExecutor jobExecutor = processEngineConfiguration.getJobExecutor();
jobExecutor.start();
try {
Timer timer = new Timer();
InteruptTask task = new InteruptTask(Thread.currentThread());
timer.schedule(task, maxMillisToWait);
boolean conditionIsViolated = true;
try {
while (conditionIsViolated) {
Thread.sleep(intervalMillis);
conditionIsViolated = !condition.call();
}
} catch (InterruptedException e) {
} catch (Exception e) {
throw new ActivitiException("Exception while waiting on condition: "+e.getMessage(), e);
} finally {
timer.cancel();
}
if (conditionIsViolated) {
throw new ActivitiException("time limit of " + maxMillisToWait + " was exceeded");
}
} finally {
jobExecutor.shutdown();
}
}
public boolean areJobsAvailable() {
return !managementService
.createJobQuery()
.executable()
.list()
.isEmpty();
}
private static class InteruptTask extends TimerTask {
protected boolean timeLimitExceeded = false;
protected Thread thread;
public InteruptTask(Thread thread) {
this.thread = thread;
}
public boolean isTimeLimitExceeded() {
return timeLimitExceeded;
}
public void run() {
timeLimitExceeded = true;
thread.interrupt();
}
}
/**
* Checks if the returned "data" array (child-node of root-json node returned by invoking a GET on the given url)
* contains entries with the given ID's.
*/
protected void assertResultsPresentInDataResponse(String url, String... expectedResourceIds) throws JsonProcessingException, IOException {
int numberOfResultsExpected = expectedResourceIds.length;
// Do the actual call
HttpResponse response = executeHttpRequest(new HttpGet(SERVER_URL_PREFIX + url), HttpStatus.SC_OK);
// Check status and size
JsonNode dataNode = objectMapper.readTree(response.getEntity().getContent()).get("data");
assertEquals(numberOfResultsExpected, dataNode.size());
// Check presence of ID's
List<String> toBeFound = new ArrayList<String>(Arrays.asList(expectedResourceIds));
Iterator<JsonNode> it = dataNode.iterator();
while(it.hasNext()) {
String id = it.next().get("id").textValue();
toBeFound.remove(id);
}
assertTrue("Not all process-definitions have been found in result, missing: " + StringUtils.join(toBeFound, ", "), toBeFound.isEmpty());
}
/**
* Checks if the returned "data" array (child-node of root-json node returned by invoking a POST on the given url)
* contains entries with the given ID's.
*/
protected void assertResultsPresentInPostDataResponse(String url, ObjectNode body, String... expectedResourceIds) throws JsonProcessingException, IOException {
assertResultsPresentInPostDataResponseWithStatusCheck(url, body, HttpStatus.SC_OK, expectedResourceIds);
}
protected void assertResultsPresentInPostDataResponseWithStatusCheck(String url, ObjectNode body, int expectedStatusCode, String... expectedResourceIds) throws JsonProcessingException, IOException {
int numberOfResultsExpected = 0;
if (expectedResourceIds != null) {
numberOfResultsExpected = expectedResourceIds.length;
}
// Do the actual call
HttpPost post = new HttpPost(SERVER_URL_PREFIX + url);
post.setEntity(new StringEntity(body.toString()));
HttpResponse response = executeHttpRequest(post, expectedStatusCode);
if (expectedStatusCode == HttpStatus.SC_OK) {
// Check status and size
JsonNode rootNode = objectMapper.readTree(response.getEntity().getContent());
JsonNode dataNode = rootNode.get("data");
assertEquals(numberOfResultsExpected, dataNode.size());
// Check presence of ID's
if (expectedResourceIds != null) {
List<String> toBeFound = new ArrayList<String>(Arrays.asList(expectedResourceIds));
Iterator<JsonNode> it = dataNode.iterator();
while(it.hasNext()) {
String id = it.next().get("id").textValue();
toBeFound.remove(id);
}
assertTrue("Not all entries have been found in result, missing: " + StringUtils.join(toBeFound, ", "), toBeFound.isEmpty());
}
}
}
/**
* Checks if the rest operation returns an error as expected
*/
protected void assertErrorResult(String url, ObjectNode body, int statusCode) throws IOException {
// Do the actual call
HttpPost post = new HttpPost(SERVER_URL_PREFIX + url);
post.setEntity(new StringEntity(body.toString()));
executeHttpRequest(post, statusCode);
}
/**
* Extract a date from the given string. Assertion fails when invalid date has been provided.
*/
protected Date getDateFromISOString(String isoString) {
DateTimeFormatter dateFormat = ISODateTimeFormat.dateTime();
try {
return dateFormat.parseDateTime(isoString).toDate();
} catch(IllegalArgumentException iae) {
fail("Illegal date provided: "+ isoString);
return null;
}
}
protected String getISODateString(Date time) {
return dateFormat.format(time);
}
}