/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.workflow.impl;
import static com.entwinemedia.fn.Stream.$;
import static org.apache.solr.client.solrj.util.ClientUtils.escapeQueryChars;
import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
import static org.opencastproject.util.data.Option.option;
import org.opencastproject.job.api.Job;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.security.api.AccessControlEntry;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AuthorizationService;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.Permissions;
import org.opencastproject.security.api.Role;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.security.util.SecurityUtil;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.solr.SolrServerFactory;
import org.opencastproject.util.JobUtil;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.SolrUtils;
import org.opencastproject.workflow.api.WorkflowDatabaseException;
import org.opencastproject.workflow.api.WorkflowException;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowParser;
import org.opencastproject.workflow.api.WorkflowQuery;
import org.opencastproject.workflow.api.WorkflowQuery.QueryTerm;
import org.opencastproject.workflow.api.WorkflowQuery.Sort;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workflow.api.WorkflowSet;
import org.opencastproject.workflow.api.WorkflowSetImpl;
import org.opencastproject.workflow.api.WorkflowStatistics;
import org.opencastproject.workflow.api.WorkflowStatistics.WorkflowDefinitionReport;
import org.opencastproject.workflow.api.WorkflowStatistics.WorkflowDefinitionReport.OperationReport;
import com.entwinemedia.fn.Fn;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.osgi.framework.ServiceException;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Provides data access to the workflow service through file storage in the workspace, indexed via solr.
*/
public class WorkflowServiceSolrIndex implements WorkflowServiceIndex {
/** The logger */
protected static final Logger logger = LoggerFactory.getLogger(WorkflowServiceSolrIndex.class);
/** Configuration key for a remote solr server */
public static final String CONFIG_SOLR_URL = "org.opencastproject.workflow.solr.url";
/** Configuration key for an embedded solr configuration and data directory */
public static final String CONFIG_SOLR_ROOT = "org.opencastproject.workflow.solr.dir";
/** Connection to the solr server. Solr is used to search for workflows. The workflow data are stored as xml files. */
protected SolrServer solrServer = null;
/** The root directory to use for solr config and data files */
protected String solrRoot = null;
/** The URL to connect to a remote solr server */
protected URL solrServerUrl = null;
/** The key in solr documents representing the workflow's current operation */
protected static final String OPERATION_KEY = "operation";
/** The <code>OPERATION_KEY</code> value used when a workflow has no current operation */
protected static final String NO_OPERATION_KEY = "none";
/** The key in solr documents representing the workflow definition id */
protected static final String WORKFLOW_DEFINITION_KEY = "templateid";
/** The key in solr documents representing the workflow's series identifier */
protected static final String SERIES_ID_KEY = "seriesid";
/** The key in solr documents representing the workflow's series title */
protected static final String SERIES_TITLE_KEY = "seriestitle";
/** The key in solr documents representing the workflow's ID */
protected static final String ID_KEY = "id";
/** The key in solr documents representing the workflow's current state */
private static final String STATE_KEY = "state";
/** The key in solr documents representing the workflow as xml */
private static final String XML_KEY = "xml";
/** The key in solr documents representing the workflow's contributors */
private static final String CONTRIBUTOR_KEY = "contributor";
/** The key in solr documents representing the workflow's mediapackage language */
private static final String LANGUAGE_KEY = "language";
/** The key in solr documents representing the workflow's mediapackage license */
private static final String LICENSE_KEY = "license";
/** The key in solr documents representing the workflow's mediapackage title */
private static final String TITLE_KEY = "title";
/** The key in solr documents representing the workflow's mediapackage identifier */
private static final String MEDIAPACKAGE_KEY = "mediapackageid";
/** The key in solr documents representing the workflow's mediapackage creators */
private static final String CREATOR_KEY = "creator";
/** The key in solr documents representing the workflow's mediapackage creation date */
private static final String CREATED_KEY = "created";
/** The key in solr documents representing the workflow's mediapackage subjects */
private static final String SUBJECT_KEY = "subject";
/** The key in solr documents representing the full text index */
private static final String FULLTEXT_KEY = "fulltext";
/**
* The key in solr documents representing the workflow's creator. This, along with the ACLs, are used for determining
* whether a user can view a workflow or not.
*/
private static final String WORKFLOW_CREATOR_KEY = "oc_creator";
/** The key in solr documents representing the organization that owns this workflow instance */
private static final String ORG_KEY = "oc_org";
/** The key in solr documents representing the prefix to an access control entry */
private static final String ACL_KEY_PREFIX = "oc_acl_";
/** The service registry, managing jobs */
private ServiceRegistry serviceRegistry = null;
/** The authorization service */
private AuthorizationService authorizationService = null;
/** The organization directory */
private OrganizationDirectoryService orgDirectory;
/** The security service */
private SecurityService securityService = null;
/** Whether to index workflows synchronously as they are stored */
protected boolean synchronousIndexing = true;
/** The thread pool to use in asynchronous indexing */
protected ExecutorService indexingExecutor;
public static final Fn<Job, Boolean> operationIsStartWorkflow = new Fn<Job, Boolean>() {
@Override
public Boolean apply(Job job) {
return WorkflowServiceImpl.Operation.START_WORKFLOW.toString().equals(job.getOperation());
}
};
/**
* Callback from the OSGi environment on component registration. The indexing behavior can be set using component
* context properties. <code>synchronousIndexing=true|false</code> determines whether threads performing workflow
* updates block on adding the workflow instances to the search index.
*
* @param cc
* the component context
*/
public void activate(ComponentContext cc) {
String solrServerUrlConfig = StringUtils.trimToNull(cc.getBundleContext().getProperty(CONFIG_SOLR_URL));
if (solrServerUrlConfig != null) {
try {
solrServerUrl = new URL(solrServerUrlConfig);
} catch (MalformedURLException e) {
throw new IllegalStateException("Unable to connect to solr at " + solrServerUrlConfig, e);
}
} else {
solrRoot = SolrServerFactory.getEmbeddedDir(cc, CONFIG_SOLR_ROOT, "workflow");
}
Object syncIndexingConfig = cc.getProperties().get("synchronousIndexing");
if (syncIndexingConfig != null && (syncIndexingConfig instanceof Boolean)) {
this.synchronousIndexing = (Boolean) syncIndexingConfig;
}
if (this.synchronousIndexing) {
logger.debug("Workflows will be added to the search index synchronously");
} else {
logger.debug("Workflows will be added to the search index asynchronously");
indexingExecutor = Executors.newSingleThreadExecutor();
}
String systemUserName = cc.getBundleContext().getProperty(SecurityUtil.PROPERTY_KEY_SYS_USER);
activate(systemUserName);
}
private long count() throws WorkflowDatabaseException {
try {
QueryResponse response = solrServer.query(new SolrQuery("*:*"));
return response.getResults().getNumFound();
} catch (SolrServerException e) {
throw new WorkflowDatabaseException(e);
}
}
/**
* Activates the index by configuring solr with the server url that must have been set previously.
*/
public void activate(String systemUserName) {
// Set up the solr server
if (solrServerUrl != null) {
solrServer = SolrServerFactory.newRemoteInstance(solrServerUrl);
} else {
try {
setupSolr(new File(solrRoot));
} catch (IOException e) {
throw new IllegalStateException("Unable to connect to solr at " + solrRoot, e);
} catch (SolrServerException e) {
throw new IllegalStateException("Unable to connect to solr at " + solrRoot, e);
}
}
// If the solr is empty, add all of the existing workflows
long instancesInSolr = 0;
try {
instancesInSolr = count();
} catch (WorkflowDatabaseException e) {
throw new IllegalStateException(e);
}
if (instancesInSolr == 0) {
logger.info("The workflow index is empty, looking for workflows to index");
// this may be a new index, so get all of the existing workflows and index them
List<Job> jobs = null;
try {
jobs = $(serviceRegistry.getJobs(WorkflowService.JOB_TYPE, null)).filter(operationIsStartWorkflow).toList();
} catch (ServiceRegistryException e) {
logger.error("Unable to load the workflows jobs: {}", e.getMessage());
throw new ServiceException(e.getMessage());
}
if (jobs.size() > 0) {
logger.info("Populating the workflow index with {} workflows", jobs.size());
int errors = 0;
for (Job job : jobs) {
if (job.getPayload() == null) {
logger.warn("Skipping restoring of workflow {}: Payload is empty", job.getId());
continue;
}
WorkflowInstance instance = null;
boolean erroneousWorkflowJob = false;
try {
instance = WorkflowParser.parseWorkflowInstance(job.getPayload());
Organization organization = orgDirectory.getOrganization(job.getOrganization());
securityService.setOrganization(organization);
securityService.setUser(SecurityUtil.createSystemUser(systemUserName, organization));
index(instance);
} catch (WorkflowDatabaseException e) {
logger.warn("Skipping restoring of workflow {}: {}", instance.getId(), e.getMessage());
erroneousWorkflowJob = true;
errors++;
} catch (Throwable e) {
logger.warn("Skipping restoring of workflow {}: {}", instance.getId(), e.getMessage());
erroneousWorkflowJob = true;
errors++;
}
// Make sure this job is not being dispatched anymore
if (erroneousWorkflowJob && JobUtil.isReadyToDispatch(job)) {
job.setStatus(Job.Status.CANCELED);
try {
serviceRegistry.updateJob(job);
logger.info("Canceled job {} because unable to restore", job);
} catch (Exception e) {
logger.error("Error updating erroneous job {}: {}", job.getId(), e.getMessage());
}
}
}
if (errors > 0)
logger.warn("Skipped {} erroneous workflows while populating the index", errors);
logger.info("Finished populating the workflow search index");
}
}
}
/**
* Prepares the embedded solr environment.
*
* @param solrRoot
* the solr root directory
*/
protected void setupSolr(File solrRoot) throws IOException, SolrServerException {
logger.debug("Setting up solr search index at {}", solrRoot);
File solrConfigDir = new File(solrRoot, "conf");
// Create the config directory
if (solrConfigDir.exists()) {
logger.debug("solr search index found at {}", solrConfigDir);
} else {
logger.debug("solr config directory doesn't exist. Creating {}", solrConfigDir);
FileUtils.forceMkdir(solrConfigDir);
}
// Make sure there is a configuration in place
copyClasspathResourceToFile("/solr/conf/protwords.txt", solrConfigDir);
copyClasspathResourceToFile("/solr/conf/schema.xml", solrConfigDir);
copyClasspathResourceToFile("/solr/conf/scripts.conf", solrConfigDir);
copyClasspathResourceToFile("/solr/conf/solrconfig.xml", solrConfigDir);
copyClasspathResourceToFile("/solr/conf/stopwords.txt", solrConfigDir);
copyClasspathResourceToFile("/solr/conf/synonyms.txt", solrConfigDir);
// Test for the existence of a data directory
File solrDataDir = new File(solrRoot, "data");
if (!solrDataDir.exists()) {
FileUtils.forceMkdir(solrDataDir);
}
// Test for the existence of the index. Note that an empty index directory will prevent solr from
// completing normal setup.
File solrIndexDir = new File(solrDataDir, "index");
if (solrIndexDir.isDirectory() && solrIndexDir.list().length == 0) {
FileUtils.deleteDirectory(solrIndexDir);
}
solrServer = SolrServerFactory.newEmbeddedInstance(solrRoot, solrDataDir);
}
/**
* Shuts down the solr index.
*/
public void deactivate() {
SolrServerFactory.shutdown(solrServer);
}
// TODO: generalize this method
private void copyClasspathResourceToFile(String classpath, File dir) {
InputStream in = null;
FileOutputStream fos = null;
try {
in = WorkflowServiceSolrIndex.class.getResourceAsStream(classpath);
File file = new File(dir, FilenameUtils.getName(classpath));
logger.debug("copying " + classpath + " to " + file);
fos = new FileOutputStream(file);
IOUtils.copy(in, fos);
} catch (IOException e) {
throw new RuntimeException("Error copying solr classpath resource to the filesystem", e);
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(fos);
}
}
public void index(final WorkflowInstance instance) throws WorkflowDatabaseException {
if (synchronousIndexing) {
try {
SolrInputDocument doc = createDocument(instance);
synchronized (solrServer) {
solrServer.add(doc);
solrServer.commit();
}
} catch (Exception e) {
throw new WorkflowDatabaseException("Unable to index workflow", e);
}
} else {
indexingExecutor.submit(new Runnable() {
@Override
public void run() {
try {
SolrInputDocument doc = createDocument(instance);
synchronized (solrServer) {
solrServer.add(doc);
// Use solr's autoCommit feature instead of committing on each document addition.
// See http://opencast.jira.com/browse/MH-7040 and
// http://osdir.com/ml/solr-user.lucene.apache.org/2009-09/msg00744.html
// solrServer.commit();
}
} catch (Exception e) {
WorkflowServiceSolrIndex.logger.warn("Unable to index {}: {}", instance, e);
}
}
});
}
}
/**
* Adds the workflow instance to the search index.
*
* @param instance
* the instance
* @return the solr input document
* @throws Exception
*/
protected SolrInputDocument createDocument(WorkflowInstance instance) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
doc.addField(ID_KEY, instance.getId());
doc.addField(WORKFLOW_DEFINITION_KEY, instance.getTemplate());
doc.addField(STATE_KEY, instance.getState().toString());
String xml = WorkflowParser.toXml(instance);
doc.addField(XML_KEY, xml);
// index the current operation if there is one. If the workflow is finished, there is no current operation, so use a
// constant
WorkflowOperationInstance op = instance.getCurrentOperation();
if (op == null) {
doc.addField(OPERATION_KEY, NO_OPERATION_KEY);
} else {
doc.addField(OPERATION_KEY, op.getTemplate());
}
MediaPackage mp = instance.getMediaPackage();
doc.addField(MEDIAPACKAGE_KEY, mp.getIdentifier().toString());
if (mp.getSeries() != null) {
doc.addField(SERIES_ID_KEY, mp.getSeries());
}
if (mp.getSeriesTitle() != null) {
doc.addField(SERIES_TITLE_KEY, mp.getSeriesTitle());
}
if (mp.getDate() != null) {
doc.addField(CREATED_KEY, mp.getDate());
}
if (mp.getTitle() != null) {
doc.addField(TITLE_KEY, mp.getTitle());
}
if (mp.getLicense() != null) {
doc.addField(LICENSE_KEY, mp.getLicense());
}
if (mp.getLanguage() != null) {
doc.addField(LANGUAGE_KEY, mp.getLanguage());
}
if (mp.getContributors() != null && mp.getContributors().length > 0) {
StringBuffer buf = new StringBuffer();
for (String contributor : mp.getContributors()) {
if (buf.length() > 0)
buf.append("; ");
buf.append(contributor);
}
doc.addField(CONTRIBUTOR_KEY, buf.toString());
}
if (mp.getCreators() != null && mp.getCreators().length > 0) {
StringBuffer buf = new StringBuffer();
for (String creator : mp.getCreators()) {
if (buf.length() > 0)
buf.append("; ");
buf.append(creator);
}
doc.addField(CREATOR_KEY, buf.toString());
}
if (mp.getSubjects() != null && mp.getSubjects().length > 0) {
StringBuffer buf = new StringBuffer();
for (String subject : mp.getSubjects()) {
if (buf.length() > 0)
buf.append("; ");
buf.append(subject);
}
doc.addField(SUBJECT_KEY, buf.toString());
}
User workflowCreator = instance.getCreator();
doc.addField(WORKFLOW_CREATOR_KEY, workflowCreator.getUsername());
doc.addField(ORG_KEY, instance.getOrganization().getId());
AccessControlList acl;
try {
acl = authorizationService.getActiveAcl(mp).getA();
} catch (Error e) {
logger.error("No security xacml found on media package {}", mp);
throw new WorkflowException(e);
}
addAuthorization(doc, acl);
return doc;
}
/**
* Adds authorization fields to the solr document.
*
* @param doc
* the solr document
* @param acl
* the access control list
*/
protected void addAuthorization(SolrInputDocument doc, AccessControlList acl) {
Map<String, List<String>> permissions = new HashMap<String, List<String>>();
// Define containers for common permissions
List<String> reads = new ArrayList<String>();
permissions.put(Permissions.Action.READ.toString(), reads);
List<String> writes = new ArrayList<String>();
permissions.put(Permissions.Action.WRITE.toString(), writes);
String adminRole = securityService.getOrganization().getAdminRole();
// The admin user can read and write
if (adminRole != null) {
reads.add(adminRole);
writes.add(adminRole);
}
for (AccessControlEntry entry : acl.getEntries()) {
if (!entry.isAllow()) {
logger.warn("Workflow service does not support denial via ACL, ignoring {}", entry);
continue;
}
List<String> actionPermissions = permissions.get(entry.getAction());
if (actionPermissions == null) {
actionPermissions = new ArrayList<String>();
permissions.put(entry.getAction(), actionPermissions);
}
actionPermissions.add(entry.getRole());
}
// Write the permissions to the solr document
for (Map.Entry<String, List<String>> entry : permissions.entrySet()) {
String fieldName = ACL_KEY_PREFIX + entry.getKey();
doc.setField(fieldName, entry.getValue());
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.impl.WorkflowServiceIndex#countWorkflowInstances(org.opencastproject.workflow.api.WorkflowInstance.WorkflowState,
* java.lang.String)
*/
@Override
public long countWorkflowInstances(WorkflowState state, String operation) throws WorkflowDatabaseException {
StringBuilder query = new StringBuilder();
// Consider the workflow state
if (state != null) {
query.append(STATE_KEY).append(":").append(escapeQueryChars(state.toString()));
}
// Consider the current operation
if (StringUtils.isNotBlank(operation)) {
if (query.length() > 0)
query.append(" AND ");
query.append(OPERATION_KEY).append(":").append(escapeQueryChars(operation));
}
// We want all available workflows for this organization
String orgId = securityService.getOrganization().getId();
if (query.length() > 0)
query.append(" AND ");
query.append(ORG_KEY).append(":").append(escapeQueryChars(orgId));
appendSolrAuthFragment(query, Permissions.Action.READ.toString());
try {
QueryResponse response = solrServer.query(new SolrQuery(query.toString()));
return response.getResults().getNumFound();
} catch (SolrServerException e) {
throw new WorkflowDatabaseException(e);
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.impl.WorkflowServiceIndex#getStatistics()
*/
@Override
public WorkflowStatistics getStatistics() throws WorkflowDatabaseException {
long total = 0;
long paused = 0;
long failed = 0;
long failing = 0;
long instantiated = 0;
long running = 0;
long stopped = 0;
long succeeded = 0;
WorkflowStatistics stats = new WorkflowStatistics();
// Get all definitions and then query for the numbers and the current operation per definition
try {
String orgId = securityService.getOrganization().getId();
StringBuilder queryString = new StringBuilder().append(ORG_KEY).append(":").append(escapeQueryChars(orgId));
appendSolrAuthFragment(queryString, Permissions.Action.WRITE.toString());
SolrQuery solrQuery = new SolrQuery(queryString.toString());
solrQuery.addFacetField(WORKFLOW_DEFINITION_KEY);
solrQuery.addFacetField(OPERATION_KEY);
solrQuery.setFacetMinCount(0);
solrQuery.setFacet(true);
QueryResponse response = solrServer.query(solrQuery);
FacetField templateFacet = response.getFacetField(WORKFLOW_DEFINITION_KEY);
FacetField operationFacet = response.getFacetField(OPERATION_KEY);
// For every template and every operation
if (templateFacet != null && templateFacet.getValues() != null) {
for (Count template : templateFacet.getValues()) {
WorkflowDefinitionReport templateReport = new WorkflowDefinitionReport();
templateReport.setId(template.getName());
long templateTotal = 0;
long templatePaused = 0;
long templateFailed = 0;
long templateFailing = 0;
long templateInstantiated = 0;
long templateRunning = 0;
long templateStopped = 0;
long templateSucceeded = 0;
if (operationFacet != null && operationFacet.getValues() != null) {
for (Count operation : operationFacet.getValues()) {
OperationReport operationReport = new OperationReport();
operationReport.setId(operation.getName());
StringBuilder baseSolrQuery = new StringBuilder().append(ORG_KEY).append(":")
.append(escapeQueryChars(orgId));
appendSolrAuthFragment(baseSolrQuery, Permissions.Action.WRITE.toString());
solrQuery = new SolrQuery(baseSolrQuery.toString());
solrQuery.addFacetField(STATE_KEY);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.FAILED);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.FAILING);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.INSTANTIATED);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.PAUSED);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.RUNNING);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.STOPPED);
solrQuery.addFacetQuery(STATE_KEY + ":" + WorkflowState.SUCCEEDED);
solrQuery.addFilterQuery(WORKFLOW_DEFINITION_KEY + ":" + template.getName());
solrQuery.addFilterQuery(OPERATION_KEY + ":" + operation.getName());
solrQuery.setFacetMinCount(0);
solrQuery.setFacet(true);
response = solrServer.query(solrQuery);
// Add the states
FacetField stateFacet = response.getFacetField(STATE_KEY);
for (Count stateValue : stateFacet.getValues()) {
WorkflowState state = WorkflowState.valueOf(stateValue.getName().toUpperCase());
templateTotal += stateValue.getCount();
total += stateValue.getCount();
switch (state) {
case FAILED:
operationReport.setFailed(stateValue.getCount());
templateFailed += stateValue.getCount();
failed += stateValue.getCount();
break;
case FAILING:
operationReport.setFailing(stateValue.getCount());
templateFailing += stateValue.getCount();
failing += stateValue.getCount();
break;
case INSTANTIATED:
operationReport.setInstantiated(stateValue.getCount());
templateInstantiated += stateValue.getCount();
instantiated += stateValue.getCount();
break;
case PAUSED:
operationReport.setPaused(stateValue.getCount());
templatePaused += stateValue.getCount();
paused += stateValue.getCount();
break;
case RUNNING:
operationReport.setRunning(stateValue.getCount());
templateRunning += stateValue.getCount();
running += stateValue.getCount();
break;
case STOPPED:
operationReport.setStopped(stateValue.getCount());
templateStopped += stateValue.getCount();
stopped += stateValue.getCount();
break;
case SUCCEEDED:
operationReport.setFinished(stateValue.getCount());
templateSucceeded += stateValue.getCount();
succeeded += stateValue.getCount();
break;
default:
throw new IllegalStateException("State '" + state + "' is not handled");
}
}
templateReport.getOperations().add(operationReport);
}
}
// Update the template statistics
templateReport.setTotal(templateTotal);
templateReport.setFailed(templateFailed);
templateReport.setFailing(templateFailing);
templateReport.setInstantiated(templateInstantiated);
templateReport.setPaused(templatePaused);
templateReport.setRunning(templateRunning);
templateReport.setStopped(templateStopped);
templateReport.setFinished(templateSucceeded);
// Add the definition report to the statistics
stats.getDefinitions().add(templateReport);
}
}
} catch (SolrServerException e) {
throw new WorkflowDatabaseException(e);
}
stats.setTotal(total);
stats.setFailed(failed);
stats.setFailing(failing);
stats.setInstantiated(instantiated);
stats.setPaused(paused);
stats.setRunning(running);
stats.setStopped(stopped);
stats.setFinished(succeeded);
return stats;
}
/**
* Appends query parameters to a solr query
*
* @param sb
* The {@link StringBuilder} containing the query
* @param key
* the key for this search parameter
* @param value
* the value for this search parameter
* @param toLowerCase
* <code>true</code> to convert the value to lower case prior to comparison
* @return the appended {@link StringBuilder}
*/
private StringBuilder append(StringBuilder sb, String key, String value, boolean toLowerCase) {
if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
return sb;
}
if (sb.length() > 0) {
sb.append(" AND ");
}
sb.append(key);
sb.append(":");
if (toLowerCase)
sb.append(escapeQueryChars(value.toLowerCase()));
else
sb.append(escapeQueryChars(value));
return sb;
}
/**
* Appends query parameters to a solr query in a way that they are found even though they are not treated as a full
* word in solr.
*
* @param sb
* The {@link StringBuilder} containing the query
* @param key
* the key for this search parameter
* @param value
* the value for this search parameter
* @return the appended {@link StringBuilder}
*/
private StringBuilder appendFuzzy(StringBuilder sb, String key, String value) {
if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
return sb;
}
if (sb.length() > 0) {
sb.append(" AND ");
}
sb.append("(");
sb.append(key).append(":").append(escapeQueryChars(value.toLowerCase()));
sb.append(" OR ");
sb.append(key).append(":*").append(escapeQueryChars(value.toLowerCase())).append("*");
sb.append(")");
return sb;
}
/**
* Appends query parameters to a solr query
*
* @param sb
* The {@link StringBuilder} containing the query
* @param key
* the key for this search parameter
* @return the appended {@link StringBuilder}
*/
private StringBuilder append(StringBuilder sb, String key, Date startDate, Date endDate) {
if (StringUtils.isBlank(key) || (startDate == null && endDate == null)) {
return sb;
}
if (sb.length() > 0) {
sb.append(" AND ");
}
if (startDate == null)
startDate = new Date(0);
if (endDate == null)
endDate = new Date(Long.MAX_VALUE);
sb.append(key);
sb.append(":");
sb.append(SolrUtils.serializeDateRange(option(startDate), option(endDate)));
return sb;
}
/**
* Builds a solr search query from a {@link WorkflowQuery}.
*
* @param query
* the workflow query
* @param action
* @param applyPermissions
* whether to apply the permissions to the query. Set to false for administrative queries.
* @return the solr query string
*/
protected String createQuery(WorkflowQuery query, String action, boolean applyPermissions)
throws WorkflowDatabaseException {
String orgId = securityService.getOrganization().getId();
StringBuilder sb = new StringBuilder().append(ORG_KEY).append(":").append(escapeQueryChars(orgId));
append(sb, ID_KEY, query.getId(), false);
append(sb, MEDIAPACKAGE_KEY, query.getMediaPackageId(), false);
append(sb, SERIES_ID_KEY, query.getSeriesId(), false);
appendFuzzy(sb, SERIES_TITLE_KEY, query.getSeriesTitle());
appendFuzzy(sb, FULLTEXT_KEY, query.getText());
append(sb, WORKFLOW_DEFINITION_KEY, query.getWorkflowDefinitionId(), false);
append(sb, CREATED_KEY, query.getFromDate(), query.getToDate());
appendFuzzy(sb, CREATOR_KEY, query.getCreator());
appendFuzzy(sb, CONTRIBUTOR_KEY, query.getContributor());
appendFuzzy(sb, LANGUAGE_KEY, query.getLanguage());
appendFuzzy(sb, LICENSE_KEY, query.getLicense());
appendFuzzy(sb, TITLE_KEY, query.getTitle());
appendFuzzy(sb, SUBJECT_KEY, query.getSubject());
appendMap(sb, OPERATION_KEY, query.getCurrentOperations());
appendMap(sb, STATE_KEY, query.getStates());
if (applyPermissions) {
appendSolrAuthFragment(sb, action);
}
// Limit the results to only those workflow instances the current user can read
logger.debug(sb.toString());
return sb.toString();
}
protected void appendSolrAuthFragment(StringBuilder sb, String action) throws WorkflowDatabaseException {
User user = securityService.getUser();
if (!user.hasRole(GLOBAL_ADMIN_ROLE) && !user.hasRole(user.getOrganization().getAdminRole())) {
sb.append(" AND ").append(ORG_KEY).append(":")
.append(escapeQueryChars(securityService.getOrganization().getId()));
Set<Role> roles = user.getRoles();
if (roles.size() > 0) {
sb.append(" AND (").append(WORKFLOW_CREATOR_KEY).append(":").append(escapeQueryChars(user.getUsername()));
for (Role role : roles) {
sb.append(" OR ");
sb.append(ACL_KEY_PREFIX).append(action).append(":").append(escapeQueryChars(role.getName()));
}
sb.append(")");
}
}
}
/**
* Returns the search index' field name that corresponds to the sort field.
*
* @param sort
* the sort field
* @return the field name in the search index
*/
protected String getSortField(Sort sort) {
switch (sort) {
case TITLE:
return TITLE_KEY;
case CONTRIBUTOR:
return CONTRIBUTOR_KEY;
case DATE_CREATED:
return CREATED_KEY;
case CREATOR:
return CREATOR_KEY;
case LANGUAGE:
return LANGUAGE_KEY;
case LICENSE:
return LICENSE_KEY;
case MEDIA_PACKAGE_ID:
return MEDIAPACKAGE_KEY;
case SERIES_ID:
return SERIES_ID_KEY;
case SERIES_TITLE:
return SERIES_TITLE_KEY;
case SUBJECT:
return SUBJECT_KEY;
case WORKFLOW_DEFINITION_ID:
return WORKFLOW_DEFINITION_KEY;
default:
throw new IllegalArgumentException("No mapping found between sort field and index");
}
}
/**
* Appends query parameters from a {@link java.util.Map} to a solr query. The map
*
* @param sb
* The {@link StringBuilder} containing the query
* @param key
* the key for this search parameter
* @return the appended {@link StringBuilder}
*/
protected StringBuilder appendMap(StringBuilder sb, String key, List<QueryTerm> queryTerms) {
if (queryTerms == null || queryTerms.isEmpty()) {
return sb;
}
if (sb.length() > 0) {
sb.append(" AND ");
}
// If we include only negatives inside parentheses, lucene won't return any results. So we need to add "*:*".
// See
// http://mail-archives.apache.org/mod_mbox/lucene-solr-user/201011.mbox/%3CAANLkTinTJLo7Y-W2kt+yxAcESf98p8DD7z7mrs4CpNo-@mail.gmail.com%3E
boolean positiveTerm = false;
sb.append("(");
for (int i = 0; i < queryTerms.size(); i++) {
QueryTerm term = queryTerms.get(i);
if (i > 0) {
if (term.isInclude()) {
sb.append(" OR ");
} else {
sb.append(" AND ");
}
}
if (term.isInclude()) {
positiveTerm = true;
} else {
sb.append("-");
}
sb.append(key);
sb.append(":");
sb.append(escapeQueryChars(term.getValue().toLowerCase()));
}
if (!positiveTerm) {
sb.append(" AND *:*");
}
sb.append(")");
return sb;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.impl.WorkflowServiceIndex#getWorkflowInstances(org.opencastproject.workflow.api.WorkflowQuery,
* String, boolean)
*/
@Override
public WorkflowSet getWorkflowInstances(WorkflowQuery query, String action, boolean applyPermissions)
throws WorkflowDatabaseException {
int count = query.getCount() > 0 ? (int) query.getCount() : 20; // default to 20 items if not specified
int startIndex = query.getStartPage() > 0 ? (int) query.getStartPage() * count : (int) query.getStartIndex();
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(count);
solrQuery.setStart(startIndex);
String solrQueryString = createQuery(query, action, applyPermissions);
solrQuery.setQuery(solrQueryString);
if (query.getSort() != null) {
ORDER order = query.isSortAscending() ? ORDER.asc : ORDER.desc;
solrQuery.addSortField(getSortField(query.getSort()) + "_sort", order);
}
if (!Sort.DATE_CREATED.equals(query.getSort())) {
solrQuery.addSortField(getSortField(Sort.DATE_CREATED) + "_sort", ORDER.desc);
}
long totalHits;
long time = System.currentTimeMillis();
WorkflowSetImpl set = null;
try {
QueryResponse response = solrServer.query(solrQuery);
SolrDocumentList items = response.getResults();
long searchTime = System.currentTimeMillis() - time;
totalHits = items.getNumFound();
set = new WorkflowSetImpl();
set.setPageSize(count);
set.setTotalCount(totalHits);
set.setStartPage(query.getStartPage());
set.setSearchTime(searchTime);
// Iterate through the results
for (SolrDocument doc : items) {
String xml = (String) doc.get(XML_KEY);
try {
set.addItem(WorkflowParser.parseWorkflowInstance(xml));
} catch (Exception e) {
throw new IllegalStateException("can not parse workflow xml", e);
}
}
} catch (Exception e) {
throw new WorkflowDatabaseException(e);
}
long totalTime = System.currentTimeMillis() - time;
logger.debug("Workflow query took {} ms", totalTime);
return set;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.impl.WorkflowServiceIndex#remove(long)
*/
@Override
public void remove(long id) throws WorkflowDatabaseException, NotFoundException {
try {
synchronized (solrServer) {
solrServer.deleteById(Long.toString(id));
solrServer.commit();
}
} catch (Exception e) {
throw new WorkflowDatabaseException(e);
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.impl.WorkflowServiceIndex#update(org.opencastproject.workflow.api.WorkflowInstance)
*/
@Override
public void update(WorkflowInstance instance) throws WorkflowDatabaseException {
index(instance);
}
/**
* Clears the index of all workflow instances.
*/
public void clear() throws WorkflowDatabaseException {
try {
synchronized (solrServer) {
solrServer.deleteByQuery("*:*");
solrServer.commit();
}
} catch (Exception e) {
throw new WorkflowDatabaseException(e);
}
}
/**
* Callback for the OSGi environment to register with the <code>ServiceRegistry</code>.
*
* @param registry
* the service registry
*/
protected void setServiceRegistry(ServiceRegistry registry) {
this.serviceRegistry = registry;
}
/**
* Callback for setting the organization directory service.
*
* @param orgDirectory
* the organization directory service
*/
protected void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
this.orgDirectory = orgDirectory;
}
/**
* Callback for setting the authorization service.
*
* @param authorizationService
* the authorizationService to set
*/
protected void setAuthorizationService(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
/**
* Callback for setting the security service.
*
* @param securityService
* the securityService to set
*/
protected void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
}