/*
* 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;
import cz.cas.lib.proarc.common.config.CatalogConfiguration;
import cz.cas.lib.proarc.common.dao.ConcurrentModificationException;
import cz.cas.lib.proarc.common.dao.DaoFactory;
import cz.cas.lib.proarc.common.dao.Transaction;
import cz.cas.lib.proarc.common.dao.WorkflowJobDao;
import cz.cas.lib.proarc.common.dao.WorkflowMaterialDao;
import cz.cas.lib.proarc.common.dao.WorkflowParameterDao;
import cz.cas.lib.proarc.common.dao.WorkflowTaskDao;
import cz.cas.lib.proarc.common.fedora.RemoteStorage;
import cz.cas.lib.proarc.common.fedora.SearchView.Item;
import cz.cas.lib.proarc.common.user.UserManager;
import cz.cas.lib.proarc.common.user.UserProfile;
import cz.cas.lib.proarc.common.workflow.model.DigitalMaterial;
import cz.cas.lib.proarc.common.workflow.model.FolderMaterial;
import cz.cas.lib.proarc.common.workflow.model.Job;
import cz.cas.lib.proarc.common.workflow.model.Job.State;
import cz.cas.lib.proarc.common.workflow.model.JobFilter;
import cz.cas.lib.proarc.common.workflow.model.JobView;
import cz.cas.lib.proarc.common.workflow.model.Material;
import cz.cas.lib.proarc.common.workflow.model.MaterialFilter;
import cz.cas.lib.proarc.common.workflow.model.MaterialType;
import cz.cas.lib.proarc.common.workflow.model.MaterialView;
import cz.cas.lib.proarc.common.workflow.model.PhysicalMaterial;
import cz.cas.lib.proarc.common.workflow.model.Task;
import cz.cas.lib.proarc.common.workflow.model.TaskFilter;
import cz.cas.lib.proarc.common.workflow.model.TaskParameter;
import cz.cas.lib.proarc.common.workflow.model.TaskParameterFilter;
import cz.cas.lib.proarc.common.workflow.model.TaskParameterView;
import cz.cas.lib.proarc.common.workflow.model.TaskView;
import cz.cas.lib.proarc.common.workflow.profile.BlockerDefinition;
import cz.cas.lib.proarc.common.workflow.profile.JobDefinition;
import cz.cas.lib.proarc.common.workflow.profile.MaterialDefinition;
import cz.cas.lib.proarc.common.workflow.profile.SetMaterialDefinition;
import cz.cas.lib.proarc.common.workflow.profile.SetParamDefinition;
import cz.cas.lib.proarc.common.workflow.profile.StepDefinition;
import cz.cas.lib.proarc.common.workflow.profile.TaskDefinition;
import cz.cas.lib.proarc.common.workflow.profile.WorkerDefinition;
import cz.cas.lib.proarc.common.workflow.profile.WorkflowDefinition;
import cz.cas.lib.proarc.common.workflow.profile.WorkflowProfiles;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Jan Pokorsky
*/
public class WorkflowManager {
private static WorkflowManager INSTANCE;
private static final Logger LOG = Logger.getLogger(WorkflowManager.class.getName());
private final DaoFactory daoFactory;
private final UserManager userMgr;
public static WorkflowManager getInstance() {
return INSTANCE;
}
public static void setInstance(WorkflowManager instance) {
INSTANCE = instance;
}
private final WorkflowProfiles wp;
public WorkflowManager(WorkflowProfiles wp, DaoFactory daoFactory, UserManager users) {
this.daoFactory = daoFactory;
this.userMgr = users;
this.wp = wp;
}
public Job getJob(BigDecimal id) {
Transaction tx = daoFactory.createTransaction();
WorkflowJobDao jobDao = daoFactory.createWorkflowJobDao();
jobDao.setTransaction(tx);
try {
return jobDao.find(id);
} finally {
tx.close();
}
}
public List<JobView> findJob(JobFilter filter) {
WorkflowDefinition wd = wp.getProfiles();
Transaction tx = daoFactory.createTransaction();
WorkflowJobDao jobDao = daoFactory.createWorkflowJobDao();
jobDao.setTransaction(tx);
try {
String lang = filter.getLocale().getLanguage();
List<JobView> jobs = jobDao.view(filter);
for (JobView job : jobs) {
JobDefinition profile = wp.getProfile(wd, job.getProfileName());
if (profile != null) {
job.setProfileLabel(profile.getTitle(lang, profile.getName()));
job.setProfileHint(profile.getHint(lang, null));
} else {
job.setProfileLabel(job.getProfileName());
job.setProfileHint("Unknown job XML ID: " + job.getProfileName());
}
}
return jobs;
} finally {
tx.close();
}
}
public List<MaterialView> findMaterial(MaterialFilter filter) {
WorkflowDefinition wd = wp.getProfiles();
Transaction tx = daoFactory.createTransaction();
WorkflowMaterialDao dao = daoFactory.createWorkflowMaterialDao();
dao.setTransaction(tx);
try {
List<MaterialView> mats = dao.view(filter);
for (MaterialView m : mats) {
MaterialDefinition matProfile = wp.getMaterialProfile(wd, m.getName());
if (matProfile != null) {
m.setProfileLabel(matProfile.getTitle(
filter.getLocale().getLanguage(),
matProfile.getName()));
} else {
m.setProfileLabel("Unknown material profile: " + m.getName());
}
}
return mats;
} finally {
tx.close();
}
}
public Material updateMaterial(MaterialView view) throws ConcurrentModificationException, WorkflowException {
Transaction tx = daoFactory.createTransaction();
WorkflowMaterialDao dao = daoFactory.createWorkflowMaterialDao();
WorkflowJobDao jobDao = daoFactory.createWorkflowJobDao();
dao.setTransaction(tx);
jobDao.setTransaction(tx);
try {
Material m = dao.find(view.getId());
if (m == null) {
throw new WorkflowException("Material not found: " + view.getId())
.addMaterialNotFound(view.getId());
}
if (m.getType() != view.getType()) {
throw new WorkflowException("Material type mismatch: "
+ view.getId() + ", " + m.getType() + "!=" + view.getType());
}
// check job state
Job job = dao.findJob(m);
if (m == null) {
throw new WorkflowException("Job not found! Material: " + view.getId());
}
if (job.getState() != State.OPEN) {
throw new WorkflowException("Job is closed!").addJobIsClosed();
}
m.setNote(view.getNote());
if (m.getType() == MaterialType.FOLDER) {
FolderMaterial fm = (FolderMaterial) m;
fm.setPath(view.getPath());
fm.setLabel(view.getPath());
} else if (m.getType() == MaterialType.DIGITAL_OBJECT) {
DigitalMaterial dm = (DigitalMaterial) m;
String label = view.getPid();
if (view.getPid() != null && !view.getPid().equals(dm.getPid())) {
List<Item> items = RemoteStorage.getInstance().getSearch().find(view.getPid());
if (!items.isEmpty()) {
label = items.get(0).getLabel();
}
}
dm.setPid(view.getPid());
dm.setLabel(label);
} else if (m.getType() == MaterialType.PHYSICAL_DOCUMENT) {
String jobLabel = job.getLabel();
PhysicalMaterial pm = (PhysicalMaterial) m;
pm.setBarcode(view.getBarcode());
pm.setField001(view.getField001());
String newMetadata = view.getMetadata();
String oldMetadata = pm.getMetadata();
if (newMetadata == null ? oldMetadata != null : !newMetadata.equals(oldMetadata)) {
PhysicalMaterial t = new PhysicalMaterialBuilder().setMetadata(newMetadata).build();
pm.setMetadata(t.getMetadata());
pm.setLabel(t.getLabel());
jobLabel = pm.getLabel();
}
pm.setRdczId(view.getRdczId());
pm.setSource(view.getSource());
jobLabel = updateMaterialSignature(view.getSignature(), pm, jobLabel);
if (jobLabel == null ? job.getLabel() != null : !jobLabel.equals(job.getLabel())) {
job.setLabel(jobLabel);
jobDao.update(job);
}
}
dao.update(m);
tx.commit();
return m;
} catch (ConcurrentModificationException t) {
tx.rollback();
throw t;
} catch (WorkflowException t) {
tx.rollback();
throw t;
} catch (Throwable t) {
tx.rollback();
throw new WorkflowException("Cannot update material: " + view.getId(), t).addUnexpectedError();
} finally {
tx.close();
}
}
static String updateMaterialSignature(String newSignature, PhysicalMaterial update, String jobLabel) {
String oldSignature = update.getSignature();
if (newSignature == null ? oldSignature != null : !newSignature.equals(oldSignature)) {
if (oldSignature != null && !oldSignature.isEmpty() && jobLabel.startsWith(oldSignature)) {
jobLabel = jobLabel.replaceFirst(String.format("^%s[ ]*", oldSignature), "");
}
if (newSignature != null) {
if ("?".equals(jobLabel) || jobLabel.isEmpty()) {
jobLabel = newSignature;
} else {
jobLabel = newSignature + ' ' + jobLabel;
}
} else if (jobLabel.isEmpty()) {
jobLabel = "?";
}
}
update.setSignature(newSignature);
return jobLabel;
}
public List<TaskParameterView> findParameter(TaskParameterFilter filter) {
WorkflowDefinition wd = wp.getProfiles();
Transaction tx = daoFactory.createTransaction();
WorkflowParameterDao dao = daoFactory.createWorkflowParameterDao();
dao.setTransaction(tx);
try {
List<TaskParameterView> params = dao.view(filter);
Task task = null;
if (params.isEmpty() && filter.getTaskId() != null) {
WorkflowTaskDao taskDao = daoFactory.createWorkflowTaskDao();
taskDao.setTransaction(tx);
task = taskDao.find(filter.getTaskId());
}
return new FilterFindParameterQuery(wp).filter(params, filter, task, wd);
} finally {
tx.close();
}
}
public Job updateJob(Job job) throws ConcurrentModificationException, WorkflowException {
if (job.getId() == null) {
throw new WorkflowException("Missing ID!");
}
Transaction tx = daoFactory.createTransaction();
WorkflowJobDao jobDao = daoFactory.createWorkflowJobDao();
WorkflowTaskDao taskDao = daoFactory.createWorkflowTaskDao();
jobDao.setTransaction(tx);
taskDao.setTransaction(tx);
try {
Job old = jobDao.find(job.getId());
if (old == null) {
throw new WorkflowException("Not found " + job.getId())
.addJobNotFound(job.getId());
}
// readonly properties
job.setCreated(old.getCreated());
job.setProfileName(old.getProfileName());
jobDao.update(job);
if (old.getState() != job.getState() && job.isClosed()) {
// close all open tasks
TaskFilter taskFilter = new TaskFilter();
taskFilter.setJobId(job.getId());
for (TaskView task : taskDao.view(taskFilter)) {
if (!task.isClosed()) {
task.setState(job.getState() == State.FINISHED
? Task.State.FINISHED : Task.State.CANCELED);
taskDao.update(task);
}
}
}
tx.commit();
return job;
} catch (ConcurrentModificationException t) {
tx.rollback();
throw t;
} catch (WorkflowException ex) {
tx.rollback();
throw ex;
} catch (Throwable t) {
tx.rollback();
throw new WorkflowException("Cannot update job: " + job.getId(), t)
.addUnexpectedError();
} finally {
tx.close();
}
}
public TaskManager tasks() {
return new TaskManager(daoFactory, wp, this);
}
public Job addJob(JobDefinition jobProfile, String xml,
CatalogConfiguration catalog, UserProfile defaultUser
) throws WorkflowException {
Map<String, UserProfile> users = createUserMap();
PhysicalMaterial physicalMaterial = new PhysicalMaterialBuilder()
.setCatalog(catalog).setMetadata(xml)
.build();
Transaction tx = daoFactory.createTransaction();
WorkflowJobDao jobDao = daoFactory.createWorkflowJobDao();
WorkflowTaskDao taskDao = daoFactory.createWorkflowTaskDao();
WorkflowParameterDao paramDao = daoFactory.createWorkflowParameterDao();
WorkflowMaterialDao materialDao = daoFactory.createWorkflowMaterialDao();
jobDao.setTransaction(tx);
taskDao.setTransaction(tx);
paramDao.setTransaction(tx);
materialDao.setTransaction(tx);
Timestamp now = new Timestamp(System.currentTimeMillis());
String jobLabel = physicalMaterial.getLabel();
try {
Job job = createJob(jobDao, now, jobLabel, jobProfile, users, defaultUser);
Map<String, Material> materialCache = new HashMap<String, Material>();
for (StepDefinition step : jobProfile.getSteps()) {
if (!step.isOptional()) {
Task task = createTask(taskDao, now, job, jobProfile, step, users, defaultUser);
createTaskParams(paramDao, step, task);
createMaterials(materialDao, step, task, materialCache, physicalMaterial);
}
}
tx.commit();
return job;
} catch (WorkflowException ex) {
tx.rollback();
throw new WorkflowException("Cannot add job: " + jobProfile.getName(), ex)
.copy(ex);
} catch (Throwable t) {
tx.rollback();
throw new WorkflowException("Cannot add job: " + jobProfile.getName(), t)
.addUnexpectedError();
} finally {
tx.close();
}
}
private Job createJob(WorkflowJobDao jobDao, Timestamp now, String jobLabel,
JobDefinition jobProfile, Map<String, UserProfile> users, UserProfile defaultUser
) throws ConcurrentModificationException {
Job job = jobDao.create().addCreated(now)
.addLabel(jobLabel)
.addOwnerId(resolveUserId(jobProfile.getWorker(), users, defaultUser, true))
.addPriority(jobProfile.getPriority())
.addProfileName(jobProfile.getName())
.setState(Job.State.OPEN)
.addTimestamp(now);
jobDao.update(job);
return job;
}
private Task createTask(WorkflowTaskDao taskDao, Timestamp now,
Job job, JobDefinition jobProfile,
StepDefinition step,
Map<String, UserProfile> users, UserProfile defaultUser
) throws ConcurrentModificationException {
Task task = taskDao.create().addCreated(now)
.addJobId(job.getId())
.addOwnerId(resolveUserId(step.getWorker(), users, defaultUser, false))
.addPriority(job.getPriority())
.setState(isBlockedNewTask(step, jobProfile) ? Task.State.WAITING : Task.State.READY)
.addTimestamp(now)
.addTypeRef(step.getTask().getName());
taskDao.update(task);
return task;
}
/**
* Checks whether there is a blocker declared as an non-optional step
* of the newly created task step.
* Do not use for DB tasks!
*/
private static boolean isBlockedNewTask(StepDefinition newTaskStep, JobDefinition jobProfile) {
for (BlockerDefinition blocker : newTaskStep.getBlockers()) {
StepDefinition blockingStep = WorkflowProfiles.findStep(jobProfile, blocker.getTask().getName());
if (blockingStep != null && !blockingStep.isOptional()) {
return true;
}
}
return false;
}
List<TaskParameter> createTaskParams(WorkflowParameterDao paramDao,
StepDefinition step, Task task) throws WorkflowException {
ArrayList<TaskParameter> params = new ArrayList<TaskParameter>();
for (SetParamDefinition setter : step.getParamSetters()) {
params.add(paramDao.create()
.addParamRef(setter.getParam().getName())
.addTaskId(task.getId())
.addValue(setter.getParam().getValueType(), setter.getValue()));
}
paramDao.add(task.getId(), params);
return params;
}
void createMaterials(WorkflowMaterialDao dao, StepDefinition step,
Task task, Map<String, Material> materialCache, PhysicalMaterial origin) {
TaskDefinition taskProfile = step.getTask();
for (SetMaterialDefinition setter : taskProfile.getMaterialSetters()) {
MaterialDefinition mProfile = setter.getMaterial();
String mName = mProfile.getName();
MaterialType mType = mProfile.getType();
Material m = materialCache.get(mName);
if (m == null) {
if (mType == MaterialType.FOLDER) {
m = new FolderMaterial();
} else if (mType == MaterialType.PHYSICAL_DOCUMENT) {
if (origin != null && origin.getId() == null) {
m = origin;
} else {
m = new PhysicalMaterial();
}
} else if (mType == MaterialType.DIGITAL_OBJECT) {
m = new DigitalMaterial();
} else {
LOG.log(Level.WARNING, "Unknown material: {0}", mName);
break;
}
m.setName(mName);
materialCache.put(mName, m);
dao.update(m);
}
dao.addTaskReference(m, task, setter.getWay());
}
}
static BigDecimal resolveUserId(WorkerDefinition worker,
Map<String, UserProfile> users, UserProfile defaultUser,
boolean emptyUserAsDefault) {
UserProfile up = resolveUser(worker, users, defaultUser, emptyUserAsDefault);
return up != null ? new BigDecimal(up.getId()) : null;
}
private static UserProfile resolveUser(WorkerDefinition worker,
Map<String, UserProfile> users, UserProfile defaultUser,
boolean emptyUserAsDefault) {
String username;
if (worker != null && worker.getActual()) {
username = defaultUser == null ? null: defaultUser.getUserName();
} else {
username = worker == null ? null : worker.getUsername();
}
UserProfile up = users.get(username);
return up != null
? up
: emptyUserAsDefault ? defaultUser : null;
}
Map<String, UserProfile> createUserMap() {
HashMap<String, UserProfile> map = new HashMap<String, UserProfile>();
for (UserProfile up : userMgr.findAll()) {
map.put(up.getUserName(), up);
}
return map;
}
}