/*******************************************************************************
* Copyright 2006 - 2014 Vienna University of Technology,
* Department of Software Technology and Interactive Systems, IFS
*
* Licensed 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 eu.scape_project.planning.plato.wf;
import java.io.File;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import org.slf4j.Logger;
import eu.scape_project.planning.bean.PrepareChangesForPersist;
import eu.scape_project.planning.exception.PlanningException;
import eu.scape_project.planning.manager.ByteStreamManager;
import eu.scape_project.planning.manager.DigitalObjectManager;
import eu.scape_project.planning.manager.PlanManager;
import eu.scape_project.planning.manager.StorageException;
import eu.scape_project.planning.model.Alternative;
import eu.scape_project.planning.model.DigitalObject;
import eu.scape_project.planning.model.Plan;
import eu.scape_project.planning.model.PlanState;
import eu.scape_project.planning.model.SampleObject;
import eu.scape_project.planning.model.User;
import eu.scape_project.planning.services.characterisation.fits.FitsIntegration;
import eu.scape_project.planning.utils.XmlXPathEvaluator;
import eu.scape_project.planning.validation.PlanValidator;
import eu.scape_project.planning.validation.ValidationError;
/**
* Base class for steps of the workflow.
*
* @author Markus Hamm, Michael Kraxner
*/
public abstract class AbstractWorkflowStep implements Serializable {
private static final long serialVersionUID = -517256120799033395L;
@Inject
private Logger log;
@Inject
protected ByteStreamManager bytestreamManager;
@Inject
protected DigitalObjectManager digitalObjectManager;
@Inject
protected EntityManager em;
@Inject
protected PlanManager planManager;
@Inject
protected PlanValidator planValidator;
@Inject
protected User user;
@Inject
protected PrepareChangesForPersist prepareChangesForPersist;
@Inject
private FitsIntegration fits;
@Inject
private XmlXPathEvaluator xmlXPathEvaluator;
protected Plan plan;
/**
* Set containing a list of bytestreams(pids) stored since the last
* action-execution (save, discard).
*/
protected Set<String> addedBytestreams = new HashSet<String>();
/**
* Set containing a list of bytestreams(pids) marked to delete since the
* last action-execution (save, discard).
*/
protected Set<String> bytestreamsToRemove = new HashSet<String>();
protected PlanState requiredPlanState;
protected PlanState correspondingPlanState;
/**
* Empty constructor.
*/
public AbstractWorkflowStep() {
}
/**
* Initializes this workflow step.
*
* @param p
* the plan
*/
public void init(Plan p) {
plan = p;
prepareChangesForPersist.setUser(user.getUsername());
}
/**
* Saves the plan. Updates the plan state if the current step is complete.
* Otherwise the plan state is reset to the current state and errors are
* added to the error list.
*
* @param errors
* a list of errors
* @return true if there were no errors, false otherwise
*/
public boolean proceed(List<ValidationError> errors) {
// save the plan - this way the state is reset to the requiredPlanState,
// and changes are not lost
save();
if (mayProceed(errors)) {
plan.getPlanProperties().setState(correspondingPlanState);
saveEntity(plan.getPlanProperties());
return errors.isEmpty();
}
return false;
}
/**
* Checks if the plan may proceed to the next step.
*
* @param errors
* a list of errors
* @return true if the plan may proceed, false otherwise
*/
protected boolean mayProceed(List<ValidationError> errors) {
return planValidator.isPlanStateSatisfied(plan, correspondingPlanState, errors);
}
/**
* Stores all changes made in a step.
*
* Resets the plan's state to requiredPlanState and persist it. Saves
* step-specific changes and cleans up added or deleted bytestreams.
*
* Note: derived steps have to store their changes in
* {@link AbstractWorkflowStep#saveStepSpecific()}
*/
public void save() {
plan.getPlanProperties().setState(requiredPlanState);
plan.getPlanProperties().touch();
saveWithoutModifyingPlanState();
}
/**
* Stores all changes made in a step WITHOUT modifying the plan-state. This
* method is often required at re-using the business-logic of a
* workflow-step.
*
* Saves plan properties, step-specific changes and cleans-up added/deleted
* bytestreams.
*/
public void saveWithoutModifyingPlanState() {
saveEntity(plan.getPlanProperties());
saveStepSpecific();
// Added bytestreams are accepted
addedBytestreams.clear();
// Delete bytestreams marked to remove
for (String pid : bytestreamsToRemove) {
try {
bytestreamManager.delete(pid);
} catch (StorageException e) {
log.error("failed to delete bytestream: " + pid);
}
}
bytestreamsToRemove.clear();
}
/**
* Persists the provided entity.
*
* @param entity
* Entity to persist
* @return The entity merged into current persistence context.
*/
protected Object saveEntity(Object entity) {
prepareChangesForPersist.prepare(entity);
Object merged = em.merge(entity);
em.persist(merged);
return merged;
}
/**
* Removes the provided entity from the database.
*
* @param entity
* Entity to remove
*/
protected void removeEntity(Object entity) {
Object merged = em.merge(entity);
em.remove(merged);
}
/**
* Discards all changes that have not been persisted so far.
*
* @throws PlanningException
* if the reload failed
*/
public void discard() throws PlanningException {
// Delete added bytestreams
for (String pid : addedBytestreams) {
try {
bytestreamManager.delete(pid);
} catch (StorageException e) {
log.error("Failed to delete discarded bytestream: " + pid);
}
}
addedBytestreams.clear();
// Clear discarded bytestreams
bytestreamsToRemove.clear();
plan = planManager.reloadPlan(plan);
}
/**
* Stores all changes made in this step. Define here what you want to save
* in derived steps.
*/
protected abstract void saveStepSpecific();
/**
* Method responsible for retrieving a copy of a previously uploaded result
* file.
*
* @param alternative
* Alternative the file was uploaded for
* @param sampleObject
* Sample the file was uploaded for
* @return A copy of the result file as DigitalObject
* @throws StorageException
* if any error occurred retrieving the result file
*/
public DigitalObject fetchResultFile(Alternative alternative, SampleObject sampleObject) throws StorageException {
DigitalObject digitalObject = alternative.getExperiment().getResults().get(sampleObject);
return digitalObjectManager.getCopyOfDataFilledDigitalObject(digitalObject);
}
/**
* Fetches a copy of the provided digital object filled with data.
*
* @param object
* the object to fetch
* @return a data filled digital object
* @throws StorageException
* if any error occurred fetching data
*/
public DigitalObject fetchDigitalObject(DigitalObject object) throws StorageException {
return digitalObjectManager.getCopyOfDataFilledDigitalObject(object);
}
/**
* Characterizes a digital object with FITS tool.
*
* @param digitalObject
* digital object to characterize
* @param updateFormatInfo
* true to update the format info, false otherwise
* @return true if characterization was successful, false otherwise
*/
public boolean characteriseFits(DigitalObject digitalObject, boolean updateFormatInfo) {
if (fits == null) {
log.debug("FITS is not available and needs to be reconfigured.");
return false;
}
if (digitalObject != null && digitalObject.isDataExistent()) {
try {
String fitsXML = null;
File sampleFile = bytestreamManager.getTempFile(digitalObject.getPid());
fitsXML = fits.characterise(sampleFile);
digitalObject.setFitsXMLString(fitsXML);
log.debug("FITS xml stored in digital-object " + digitalObject.getFullname());
if (updateFormatInfo) {
return updateFormatInformationBasedOnFits(digitalObject);
}
return true;
} catch (PlanningException e) {
log.error("characterisation with FITS failed.", e);
return false;
}
}
return false;
}
/**
* Characterizes a digital object with FITS tool and updates the format
* info.
*
* @param digitalObject
* digital object to characterize
* @return true if characterization was successful, false otherwise
*/
public boolean characteriseFits(DigitalObject digitalObject) {
return characteriseFits(digitalObject, true);
}
/**
* Updates the format info of the digital object based on the FITS string.
*
* @param digitalObject
* digital object to update
* @return true if the format information was updated, false otherwise
*/
private boolean updateFormatInformationBasedOnFits(DigitalObject digitalObject) {
if (digitalObject == null || digitalObject.getFitsXMLString() == null
|| digitalObject.getFitsXMLString().isEmpty()) {
log.debug("Invalid sample object passed to process - stop update format-information.");
return false;
}
try {
xmlXPathEvaluator.setXmlToParse(digitalObject.getFitsXMLString());
digitalObject.getFormatInfo().setPuid(
xmlXPathEvaluator.extractValue("/fits/identification/identity/externalIdentifier[@type='puid']"));
digitalObject.getFormatInfo().setName(
xmlXPathEvaluator.extractValue("/fits/identification/identity/attribute::format"));
digitalObject.getFormatInfo().setVersion(
xmlXPathEvaluator.extractValue("/fits/identification/identity/version[@toolname='Jhove']"));
digitalObject.getFormatInfo().setMimeType(
xmlXPathEvaluator.extractValue("/fits/identification/identity/attribute::mimetype"));
digitalObject.getFormatInfo().setDefaultExtension(extractFileExtension(digitalObject.getFullname()));
digitalObject.getFormatInfo().touch();
log.debug("Successfully updated fomat-information based on FITS for digital object "
+ digitalObject.getFullname());
return true;
} catch (Exception e) {
log.error(
"Error at updating fomat-information based on FITS for digital object " + digitalObject.getFullname(),
e);
return false;
}
}
/**
* Extracts the file extension of a given filename.
*
* @param fileName
* filename to parse
* @return file extension or an empty string if the filename has no
* extension
*/
private String extractFileExtension(String fileName) {
int separatorIndex = fileName.lastIndexOf('.');
// if no extension is present in the filename
if (separatorIndex == -1 || separatorIndex == (fileName.length() - 1)) {
return "";
}
return fileName.substring(separatorIndex + 1);
}
// ********** getter/setter **********
public void setPlan(Plan p) {
this.plan = p;
}
public Plan getPlan() {
return this.plan;
}
}