/*
* Copyright (C) 2015 Jan Pokorsky
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.cas.lib.proarc.common.workflow.profile;
import cz.cas.lib.proarc.common.i18n.BundleValue;
import cz.cas.lib.proarc.common.i18n.BundleValueMap;
import cz.cas.lib.proarc.common.object.ValueMap;
import cz.cas.lib.proarc.common.workflow.profile.ValueMapDefinition.ValueMapItemDefinition;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.util.ValidationEventCollector;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.apache.commons.io.FileUtils;
import org.xml.sax.SAXException;
/**
* Reads actual configuration of workflow profiles.
*
* @author Jan Pokorsky
*/
public class WorkflowProfiles {
private static final Logger LOG = Logger.getLogger(WorkflowProfiles.class.getName());
private static WorkflowProfiles INSTANCE;
private final File file;
private long lastModified;
private WorkflowDefinition profiles;
public static WorkflowProfiles getInstance() {
return INSTANCE;
}
public static void setInstance(WorkflowProfiles wp) {
INSTANCE = wp;
}
/**
* Creates a new workflow definition file if not exists.
* @param target
* @throws IOException error
*/
public static void copyDefaultFile(File target) throws IOException {
if (target.exists()) {
return ;
}
FileUtils.copyURLToFile(WorkflowProfiles.class.getResource("workflow.xml"), target);
}
public WorkflowProfiles(File file) {
if (file == null) {
throw new NullPointerException();
}
this.file = file;
}
/**
* Gets actual profiles or {@code null} if in case of an error.
*/
public synchronized WorkflowDefinition getProfiles() {
try {
read();
return profiles;
} catch (JAXBException ex) {
LOG.log(Level.SEVERE, file.toString(), ex);
return null;
}
}
public JobDefinition getProfile(WorkflowDefinition workflow, String name) {
for (JobDefinition job : workflow.getJobs()) {
if (job.getName().equals(name)) {
return job;
}
}
return null;
}
public TaskDefinition getTaskProfile(WorkflowDefinition workflow, String taskName) {
for (TaskDefinition task : workflow.getTasks()) {
if (task.getName().equals(taskName)) {
return task;
}
}
return null;
}
/**
* Finds job's step.
* @return the step or {@code null}
*/
public static StepDefinition findStep(JobDefinition job, String stepName) {
for (StepDefinition step : job.getSteps()) {
if (stepName.equals(step.getTask().getName())) {
return step;
}
}
return null;
}
public ParamDefinition getParamProfile(TaskDefinition task, String paramName) {
for (ParamDefinition paramDef : task.getParams()) {
if (paramDef.getName().equals(paramName)) {
return paramDef;
}
}
return null;
}
public MaterialDefinition getMaterialProfile(WorkflowDefinition workflow, String materialName) {
for (MaterialDefinition md : workflow.getMaterials()) {
if (md.getName().equals(materialName)) {
return md;
}
}
return null;
}
public List<ValueMap> getValueMap(ValueMap.Context ctx) {
WorkflowDefinition wd = getProfiles();
if (wd == null) {
return Collections.emptyList();
}
ArrayList<ValueMap> result = new ArrayList<ValueMap>();
for (ValueMapDefinition vmd : wd.getValueMaps()) {
if (vmd.getSource() == ValueMapSource.INTERNAL) {
String id = vmd.getId();
List<ValueMapItemDefinition> items = vmd.getItems();
ArrayList<BundleValue> bitems = new ArrayList<BundleValue>();
for (ValueMapItemDefinition vmitem : items) {
String key = vmitem.getKey() != null ? vmitem.getKey() : vmitem.getValue();
bitems.add(new BundleValue(key, vmitem.getValue()));
}
BundleValueMap bundleValueMap = new BundleValueMap();
bundleValueMap.setMapId(id);
bundleValueMap.setValues(bitems);
result.add(bundleValueMap);
}
}
result.add(getAllTaskValueMap(wd, ctx));
return result;
}
private ValueMap<WorkflowItemView> getAllTaskValueMap(WorkflowDefinition wd, ValueMap.Context ctx) {
String lang = ctx.getLocale().getLanguage();
ArrayList<WorkflowItemView> taskList = new ArrayList<WorkflowItemView>(wd.getTasks().size());
for (TaskDefinition task : wd.getTasks()) {
taskList.add(new WorkflowItemView(task, lang));
}
Collections.sort(taskList, new WorkflowItemComparator(ctx.getLocale()));
return new ValueMap<WorkflowItemView>(
WorkflowProfileConsts.WORKFLOWITEMVIEW_TASKS_VALUEMAP, taskList);
}
/**
* Gets a list of sorted job's task names. The order of tasks is driven by
* the position of their step declaration in XML and by their blockers.
*/
public List<String> getSortedTaskNames(JobDefinition job) {
List<String> sortedTasks = new ArrayList<String>(job.getSteps().size());
// tasks sorted by step declaration; taskName->blockers
Map<String, Set<String>> taskDeps = new LinkedHashMap<String, Set<String>>();
for (StepDefinition step : job.getSteps()) {
String stepTaskName = step.getTask().getName();
Set<String> blockers = taskDeps.get(stepTaskName);
if (blockers == null) {
blockers = new HashSet<String>();
taskDeps.put(stepTaskName, blockers);
} else {
throw new IllegalStateException("A duplicate step declaration: "
+ job.getName() + ", " + stepTaskName);
}
for (BlockerDefinition blocker : step.getBlockers()) {
if (stepTaskName.equals(blocker.getTask().getName())) {
// short cycle, ignore
continue;
}
blockers.add(blocker.getTask().getName());
}
}
while (!taskDeps.isEmpty()) {
List<String> shakedOffDeps = shakeOffUnblockedTasks(taskDeps);
if (shakedOffDeps.isEmpty() && !taskDeps.isEmpty()) {
throw new IllegalStateException("There must be a cycle: "
+ job.getName() + ", " + taskDeps);
}
sortedTasks.addAll(shakedOffDeps);
}
return sortedTasks;
}
/**
* Removes unblocked tasks from the map and returns their ordered names.
*/
private List<String> shakeOffUnblockedTasks(Map<String, Set<String>> taskDeps) {
List<String> sortedTasks = new ArrayList<String>();
for (Entry<String, Set<String>> taskEntry : taskDeps.entrySet()) {
String taskName = taskEntry.getKey();
Set<String> blockers = taskEntry.getValue();
for (Iterator<String> it = blockers.iterator(); it.hasNext();) {
if (!taskDeps.containsKey(it.next())) {
it.remove();
}
}
if (blockers.isEmpty()) {
sortedTasks.add(taskName);
}
}
// remove resolved tasks
for (String sortedTask : sortedTasks) {
taskDeps.remove(sortedTask);
}
return sortedTasks;
}
private synchronized void setProfiles(WorkflowDefinition profiles, long time) {
if (time != lastModified) {
this.profiles = profiles;
this.lastModified = time;
}
}
private void read() throws JAXBException {
long currentTime = file.lastModified();
if (currentTime == lastModified) {
return ;
}
Unmarshaller unmarshaller = getUnmarshaller();
ValidationEventCollector errors = (ValidationEventCollector) unmarshaller.getEventHandler();
WorkflowDefinition fetchedWf = null;
try {
WorkflowDefinition wf = (WorkflowDefinition) unmarshaller.unmarshal(file);
if (!errors.hasEvents()) {
readCaches(wf);
fetchedWf = wf;
}
} catch (UnmarshalException ex) {
if (!errors.hasEvents()) {
throw ex;
}
} finally {
setProfiles(fetchedWf, currentTime);
}
if (errors.hasEvents()) {
StringBuilder err = new StringBuilder();
for (ValidationEvent event : errors.getEvents()) {
err.append(event).append('\n');
}
throw new JAXBException(err.toString());
}
}
private void readCaches(WorkflowDefinition wf) {
if (wf == null) {
return ;
}
for (JobDefinition job : wf.getJobs()) {
job.setTaskNamesSortedByBlockers(Collections.unmodifiableList(getSortedTaskNames(job)));
}
}
private Unmarshaller getUnmarshaller() throws JAXBException {
JAXBContext jctx = JAXBContext.newInstance(WorkflowDefinition.class);
Unmarshaller unmarshaller = jctx.createUnmarshaller();
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL schemaUrl = WorkflowDefinition.class.getResource("workflow.xsd");
Schema schema = null;
try {
schema = sf.newSchema(new StreamSource(schemaUrl.toExternalForm()));
} catch (SAXException ex) {
throw new JAXBException("Missing schema workflow.xsd!", ex);
}
unmarshaller.setSchema(schema);
ValidationEventCollector errors = new ValidationEventCollector() {
@Override
public boolean handleEvent(ValidationEvent event) {
super.handleEvent(event);
return true;
}
};
unmarshaller.setEventHandler(errors);
return unmarshaller;
}
}