package edu.unc.lib.deposit.work;
import static edu.unc.lib.dl.util.DepositConstants.DESCRIPTION_DIR;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.hp.hpl.jena.query.Dataset;
import com.hp.hpl.jena.query.ReadWrite;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import edu.unc.lib.dl.fedora.PID;
import edu.unc.lib.dl.util.DepositConstants;
import edu.unc.lib.dl.util.DepositStatusFactory;
import edu.unc.lib.dl.util.JobStatusFactory;
import edu.unc.lib.dl.util.PremisEventLogger;
import edu.unc.lib.dl.util.PremisEventLogger.Type;
import edu.unc.lib.dl.util.RedisWorkerConstants.DepositState;
import edu.unc.lib.dl.xml.JDOMNamespaceUtil;
/**
* Constructed with deposit directory and deposit ID. Facilitates event logging
* with standard success/failure states.
*
* @author count0
*
*/
public abstract class AbstractDepositJob implements Runnable {
private static final Logger log = LoggerFactory
.getLogger(AbstractDepositJob.class);
public static final String DEPOSIT_QUEUE = "Deposit";
@Autowired
private JobStatusFactory jobStatusFactory;
@Autowired
private DepositStatusFactory depositStatusFactory;
// UUID for this deposit and its deposit record
private String depositUUID;
// UUID for this ingest job
private String jobUUID;
// Root directory where all deposits are stored
@Autowired
private File depositsDirectory;
// Directory for this deposit
private File depositDirectory;
// Directory for local data files
private File dataDirectory;
private final PremisEventLogger eventLog = new PremisEventLogger(this
.getClass().getName());
// Directory containing PREMIS event files for individual objects in this
// deposit
private File eventsDirectory;
@Autowired
private Dataset dataset;
public AbstractDepositJob() {
}
public AbstractDepositJob(String uuid, String depositUUID) {
log.debug("Deposit job created: job:{} deposit:{}", uuid, depositUUID);
this.jobUUID = uuid;
this.depositUUID = depositUUID;
}
@PostConstruct
public void init() {
this.depositDirectory = new File(depositsDirectory, depositUUID);
this.dataDirectory = new File(depositDirectory,
DepositConstants.DATA_DIR);
this.eventsDirectory = new File(depositDirectory,
DepositConstants.EVENTS_DIR);
}
@Override
public final void run() {
try {
runJob();
if (dataset.isInTransaction()) {
dataset.commit();
}
} catch (Throwable e) {
if (dataset.isInTransaction()) {
dataset.abort();
}
throw e;
} finally {
dataset.end();
}
}
public abstract void runJob();
public String getDepositUUID() {
return depositUUID;
}
public void setDepositUUID(String depositUUID) {
this.depositUUID = depositUUID;
}
public PID getDepositPID() {
return new PID("uuid:" + this.depositUUID);
}
public String getJobUUID() {
return jobUUID;
}
public void setJobUUID(String uuid) {
this.jobUUID = uuid;
}
protected JobStatusFactory getJobStatusFactory() {
return jobStatusFactory;
}
public void setJobStatusFactory(JobStatusFactory jobStatusFactory) {
this.jobStatusFactory = jobStatusFactory;
}
protected DepositStatusFactory getDepositStatusFactory() {
return depositStatusFactory;
}
public void setDepositStatusFactory(
DepositStatusFactory depositStatusFactory) {
this.depositStatusFactory = depositStatusFactory;
}
public Map<String, String> getDepositStatus() {
Map<String, String> result = this.getDepositStatusFactory().get(
depositUUID);
return Collections.unmodifiableMap(result);
}
public File getDescriptionDir() {
return new File(getDepositDirectory(), DESCRIPTION_DIR);
}
public File getDepositsDirectory() {
return depositsDirectory;
}
public File getDepositDirectory() {
return depositDirectory;
}
public void setDepositDirectory(File depositDirectory) {
this.depositDirectory = depositDirectory;
}
public File getDataDirectory() {
return dataDirectory;
}
public PremisEventLogger getEventLog() {
return eventLog;
}
public File getEventsDirectory() {
return eventsDirectory;
}
/**
* Returns the manifest URIs for this deposit, or an empty list in case there are no manifests.
*
* @return
*/
public List<String> getManifestFileURIs() {
List<String> filePaths = depositStatusFactory.getManifestURIs(getDepositUUID());
return filePaths;
}
public void recordDepositEvent(Type type, String messageformat, Object... args) {
String message = MessageFormat.format(messageformat, args);
Element event = getEventLog().logEvent(type, message,
this.getDepositPID());
log.debug("event recorded: {}", event);
appendDepositEvent(getDepositPID(), event);
}
public void failJob(String message, String details) {
log.debug("failed deposit: {}", message);
throw new JobFailedException(message, details);
}
public void failJob(Throwable throwable, String messageformat, Object... args) {
String message = MessageFormat.format(messageformat, args);
log.debug("failed deposit: {}", message, throwable);
throw new JobFailedException(message, throwable);
}
protected void verifyRunning() {
DepositState state = getDepositStatusFactory().getState(getDepositUUID());
if (!DepositState.running.equals(state)) {
throw new JobInterruptedException("State for job " + getDepositUUID()
+ " is no longer running, interrupting");
}
}
/**
* Returns the PREMIS events file for the given PID
*
* @param pid
* @return
*/
protected File getEventsFile(PID pid) {
return createOrAppendToEventsFile(pid, null);
}
/**
* Appends an event to the PREMIS log for the given pid. If the log does not exist, it is created
*
* @param pid
* @param event
*/
protected void appendDepositEvent(PID pid, Element event) {
createOrAppendToEventsFile(pid, event);
}
/**
* Appends an event to the PREMIS document for the given PID, creating the document if it does not already exist or
* is corrupt.
*
* @param pid
* @param event
* @return the premis document file
*/
private File createOrAppendToEventsFile(PID pid, Element event) {
File file = new File(depositDirectory, DepositConstants.EVENTS_DIR + "/" + pid.getUUID() + ".xml");
try {
Document dom;
if (!file.exists()) {
file.getParentFile().mkdirs();
dom = createNewEventsFile(pid, file);
} else {
// Not appending anything, so return before attempting to load existing file
if (event == null)
return file;
try {
dom = new SAXBuilder().build(file);
} catch (JDOMException e) {
log.warn("Failed to parse existing events file for {}, backing up corrupt log and creating a new log", e);
try {
Files.move(file.toPath(), Paths.get(file.getAbsolutePath() + ".backup." + System.currentTimeMillis()));
} catch (IOException e2) {
failJob(e2, "Failed to backup corrupt log file for object {}.", pid);
}
dom = createNewEventsFile(pid, file);
}
}
if (event != null) {
dom.getRootElement().addContent(event.detach());
}
try (FileOutputStream out = new FileOutputStream(file, false)) {
new XMLOutputter(Format.getPrettyFormat()).output(dom, out);
}
return file;
} catch (IOException e) {
failJob(e, "Unexpected problem with deposit events file {}.", file.getAbsoluteFile());
}
return null;
}
/**
* Creates a new PREMIS event log file for the given PID using the provided file instance
*
* @param pid
* @param file
* @return Returns the document representing the created PREMIS event log.
* @throws IOException
*/
private Document createNewEventsFile(PID pid, File file) throws IOException {
file.createNewFile();
Document dom = new Document();
Element premis = new Element("premis", JDOMNamespaceUtil.PREMIS_V2_NS).addContent(PremisEventLogger
.getObjectElement(pid));
dom.setRootElement(premis);
return dom;
}
public Model getWritableModel() {
String uri = getDepositPID().getURI();
this.dataset.begin(ReadWrite.WRITE);
if (!this.dataset.containsNamedModel(uri)) {
this.dataset.addNamedModel(uri, ModelFactory.createDefaultModel());
}
return this.dataset.getNamedModel(uri).begin();
}
public Model getReadOnlyModel() {
String uri = getDepositPID().getURI();
this.dataset.begin(ReadWrite.READ);
return this.dataset.getNamedModel(uri).begin();
}
public void closeModel() {
if (dataset.isInTransaction()) {
dataset.commit();
dataset.end();
}
}
public void destroyModel() {
String uri = getDepositPID().getURI();
if (!dataset.isInTransaction()) {
getWritableModel();
}
if (this.dataset.containsNamedModel(uri)) {
this.dataset.removeNamedModel(uri);
}
}
protected void setTotalClicks(int totalClicks) {
getJobStatusFactory().setTotalCompletion(getJobUUID(), totalClicks);
}
protected void addClicks(int clicks) {
getJobStatusFactory().incrCompletion(getJobUUID(), clicks);
}
public File getSubdir(String subpath) {
return new File(getDepositDirectory(), subpath);
}
}