fedora.server.management.DefaultManagement.java Source code

Java tutorial

Introduction

Here is the source code for fedora.server.management.DefaultManagement.java

Source

/* The contents of this file are subject to the license and copyright terms
 * detailed in the license directory at the root of the source tree (also
 * available online at http://fedora-commons.org/license/).
 */
package fedora.server.management;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;

import java.net.URI;
import java.net.URISyntaxException;

import java.text.SimpleDateFormat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

import org.apache.commons.betwixt.XMLUtils;

import org.apache.log4j.Logger;

import org.jrdf.graph.URIReference;

import org.w3c.dom.Document;

import fedora.common.Constants;
import fedora.common.PID;
import fedora.common.rdf.SimpleURIReference;

import fedora.server.Context;
import fedora.server.RecoveryContext;
import fedora.server.Server;
import fedora.server.errors.GeneralException;
import fedora.server.errors.InvalidStateException;
import fedora.server.errors.InvalidXMLNameException;
import fedora.server.errors.ServerException;
import fedora.server.errors.StreamReadException;
import fedora.server.errors.StreamWriteException;
import fedora.server.errors.ValidationException;
import fedora.server.errors.authorization.AuthzException;
import fedora.server.security.Authorization;
import fedora.server.storage.ContentManagerParams;
import fedora.server.storage.DOManager;
import fedora.server.storage.DOReader;
import fedora.server.storage.DOWriter;
import fedora.server.storage.ExternalContentManager;
import fedora.server.storage.types.AuditRecord;
import fedora.server.storage.types.Datastream;
import fedora.server.storage.types.DatastreamManagedContent;
import fedora.server.storage.types.DatastreamReferencedContent;
import fedora.server.storage.types.DatastreamXMLMetadata;
import fedora.server.storage.types.MIMETypedStream;
import fedora.server.storage.types.RelationshipTuple;
import fedora.server.utilities.StreamUtility;
import fedora.server.validation.ValidationConstants;
import fedora.server.validation.ValidationUtility;

/**
 * Implements API-M without regard to the transport/messaging protocol.
 *
 * @author Chris Wilper
 * @version $Id$
 */
public class DefaultManagement implements Constants, Management, ManagementDelegate {

    /** Logger for this class. */
    private static Logger LOG = Logger.getLogger(DefaultManagement.class.getName());

    private final Authorization m_authz;

    private final DOManager m_manager;

    private final ExternalContentManager m_contentManager;

    private final int m_uploadStorageMinutes;

    private int m_lastId;

    private final File m_tempDir;

    private final Hashtable<String, Long> m_uploadStartTime;

    private long m_lastPurgeInMillis = System.currentTimeMillis();

    private final long m_purgeDelayInMillis;

    /**
     * @param purgeDelayInMillis milliseconds to delay before removing
     *                           old uploaded files
     * @author Frederic Buffet & Tommy Bourdin (Atos Worldline)
     * @date   August 1, 2008
     */
    public DefaultManagement(Authorization authz, DOManager doMgr, ExternalContentManager ecMgr, int uploadMinutes,
            int lastId, File tempDir, Hashtable<String, Long> uploadStartTime, long purgeDelayInMillis) {
        m_authz = authz;
        m_manager = doMgr;
        m_contentManager = ecMgr;
        m_uploadStorageMinutes = uploadMinutes;
        m_lastId = lastId;
        m_tempDir = tempDir;
        m_uploadStartTime = uploadStartTime;
        m_purgeDelayInMillis = purgeDelayInMillis;
    }

    public String ingest(Context context, InputStream serialization, String logMessage, String format,
            String encoding, boolean newPid) throws ServerException {
        DOWriter w = null;
        try {
            LOG.debug("Entered ingest");
            w = m_manager.getIngestWriter(Server.USE_DEFINITIVE_STORE, context, serialization, format, encoding,
                    newPid);
            String pid = w.GetObjectPID();

            m_authz.enforceIngest(context, pid, format, encoding);

            // Only create an audit record if there is a log message to capture
            if (logMessage != null && !logMessage.equals("")) {
                Date nowUTC = Server.getCurrentDate(context);
                addAuditRecord(context, w, "ingest", "", logMessage, nowUTC);
            }

            w.commit(logMessage);
            return pid;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed ingest(");
                logMsg.append("objectXML");
                logMsg.append(", format: ").append(format);
                logMsg.append(", encoding: ").append(encoding);
                logMsg.append(", newPid: ").append(newPid);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "ingest");
        }
    }

    private void finishModification(DOWriter w, String method) throws ServerException {
        if (w != null) {
            m_manager.releaseWriter(w);
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Exiting " + method);
            Runtime r = Runtime.getRuntime();
            LOG.debug("Memory: " + r.freeMemory() + " bytes free of " + r.totalMemory() + " available.");
        }
    }

    public Date modifyObject(Context context, String pid, String state, String label, String ownerId,
            String logMessage) throws ServerException {
        DOWriter w = null;
        try {
            LOG.debug("Entered modifyObject");

            m_authz.enforceModifyObject(context, pid, state, ownerId);

            checkObjectLabel(label);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            if (state != null && !state.equals("")) {
                if (!state.equals("A") && !state.equals("D") && !state.equals("I")) {
                    throw new InvalidStateException(
                            "The object state of \"" + state + "\" is invalid. The allowed values for state are: "
                                    + " A (active), D (deleted), and I (inactive).");
                }
                w.setState(state);
            }

            if (label != null) {
                w.setLabel(label);
            }
            if (ownerId != null) {
                w.setOwnerId(ownerId);
            }

            // Update audit trail
            Date nowUTC = Server.getCurrentDate(context);
            addAuditRecord(context, w, "modifyObject", "", logMessage, nowUTC);

            w.commit(logMessage);
            return w.getLastModDate();
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed modifyObject(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", state: ").append(state);
                logMsg.append(", label: ").append(label);
                logMsg.append(", ownderId: ").append(ownerId);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "modifyObject");
        }
    }

    public InputStream getObjectXML(Context context, String pid, String encoding) throws ServerException {
        try {
            LOG.debug("Entered getObjectXML");

            m_authz.enforceGetObjectXML(context, pid, encoding);

            DOReader reader = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, pid);
            InputStream instream = reader.GetObjectXML();
            return instream;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed getObjectXML(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", encoding: ").append(encoding);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting getObjectXML");
        }
    }

    public InputStream export(Context context, String pid, String format, String exportContext, String encoding)
            throws ServerException {
        try {
            LOG.debug("Entered export");

            m_authz.enforceExport(context, pid, format, exportContext, encoding);

            DOReader reader = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, pid);
            InputStream instream = reader.Export(format, exportContext);

            return instream;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed export(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", format: ").append(format);
                logMsg.append(", exportContext: ").append(exportContext);
                logMsg.append(", encoding: ").append(encoding);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting export");
        }
    }

    public Date purgeObject(Context context, String pid, String logMessage, boolean force) throws ServerException {
        if (force) {
            throw new GeneralException("Forced object removal is not " + "yet supported.");
        }
        DOWriter w = null;
        try {
            LOG.debug("Entered purgeObject");

            m_authz.enforcePurgeObject(context, pid);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            w.remove();
            w.commit(logMessage);
            Date serverDate = Server.getCurrentDate(context);
            return serverDate;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed purgeObject(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "purgeObject");
        }
    }

    public String addDatastream(Context context, String pid, String dsID, String[] altIDs, String dsLabel,
            boolean versionable, String MIMEType, String formatURI, String dsLocation, String controlGroup,
            String dsState, String checksumType, String checksum, String logMessage) throws ServerException {

        LOG.debug("Entered addDatastream");

        // empty MIME types are allowed. assume they meant "" if they provide it
        // as null.
        if (MIMEType == null) {
            MIMEType = "";
        }

        // empty altIDs are allowed. assume they meant String[0] if they provide
        // it as null.
        if (altIDs == null) {
            altIDs = new String[0];
        }

        // If the datastream ID is not specified directly, see
        // if we can get it from the RecoveryContext
        if (dsID == null && context instanceof RecoveryContext) {
            RecoveryContext rContext = (RecoveryContext) context;
            dsID = rContext.getRecoveryValue(Constants.RECOVERY.DATASTREAM_ID.uri);
            if (dsID != null) {
                LOG.debug("Using new dsID from recovery context");
            }
        }

        // check for valid xml name for datastream ID
        if (dsID != null) {
            if (!XMLUtils.isWellFormedXMLName(dsID)) {
                throw new InvalidXMLNameException("Invalid syntax for datastream ID. " + "The datastream ID of \""
                        + dsID + "\" is" + "not a valid XML Name");
            }
        }

        if (dsID != null && (dsID.equals("AUDIT") || dsID.equals("FEDORA-AUDITTRAIL"))) {
            throw new GeneralException("Creation of a datastream with an"
                    + " identifier of 'AUDIT' or 'FEDORA-AUDITTRAIL' is not permitted.");
        }
        DOWriter w = null;
        try {
            m_authz.enforceAddDatastream(context, pid, dsID, altIDs, MIMEType, formatURI, dsLocation, controlGroup,
                    dsState, checksumType, checksum);

            checkDatastreamID(dsID);
            checkDatastreamLabel(dsLabel);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            Datastream ds;
            if (controlGroup.equals("X")) {
                ds = new DatastreamXMLMetadata();
                ds.DSInfoType = ""; // field is now deprecated
                try {
                    InputStream in;
                    MIMETypedStream mimeTypedStream = null;
                    if (dsLocation.startsWith(DatastreamManagedContent.UPLOADED_SCHEME)) {
                        in = getTempStream(dsLocation);
                    } else {
                        ContentManagerParams params = new ContentManagerParams(dsLocation);
                        params.setContext(context);
                        mimeTypedStream = m_contentManager.getExternalContent(params);
                        in = mimeTypedStream.getStream();
                    }
                    // set and validate the content
                    DatastreamXMLMetadata dsm = (DatastreamXMLMetadata) ds;
                    dsm.xmlContent = getEmbeddableXML(in);
                    ValidationUtility.validateReservedDatastream(PID.getInstance(pid), dsID,
                            dsm.getContentStream());
                    if (mimeTypedStream != null) {
                        mimeTypedStream.close();
                    }
                } catch (Exception e) {
                    String extraInfo;
                    if (e.getMessage() == null) {
                        extraInfo = "";
                    } else {
                        extraInfo = " : " + e.getMessage();
                    }
                    throw new GeneralException("Error with " + dsLocation + extraInfo);
                }
            } else if (controlGroup.equals("M")) {
                ds = new DatastreamManagedContent();
                ds.DSInfoType = "DATA";
            } else if (controlGroup.equals("R") || controlGroup.equals("E")) {
                ds = new DatastreamReferencedContent();
                ds.DSInfoType = "DATA";
            } else {
                throw new GeneralException("Invalid control group: " + controlGroup);
            }
            ds.isNew = true;
            ds.DSControlGrp = controlGroup;
            ds.DSVersionable = versionable;
            if (!dsState.equals("A") && !dsState.equals("D") && !dsState.equals("I")) {
                throw new InvalidStateException(
                        "The datastream state of \"" + dsState + "\" is invalid. The allowed values for state are: "
                                + " A (active), D (deleted), and I (inactive).");
            }
            ds.DSState = dsState;
            // set new datastream id if not provided...
            if (dsID == null || dsID.length() == 0) {
                ds.DatastreamID = w.newDatastreamID();
            } else {
                if (dsID.indexOf(" ") != -1) {
                    throw new GeneralException("Datastream ids cannot contain spaces.");
                }
                if (dsID.indexOf("+") != -1) {
                    throw new GeneralException("Datastream ids cannot contain plusses.");
                }
                if (dsID.indexOf(":") != -1) {
                    throw new GeneralException("Datastream ids cannot contain colons.");
                }
                if (w.GetDatastream(dsID, null) != null) {
                    throw new GeneralException("A datastream already exists with ID: " + dsID);
                } else {
                    ds.DatastreamID = dsID;
                }
            }
            // add version level attributes and
            // create new ds version id ...
            ds.DSVersionID = ds.DatastreamID + ".0";
            ds.DSLabel = dsLabel;
            ds.DSLocation = dsLocation;
            if (dsLocation != null) {
                ValidationUtility.validateURL(dsLocation, ds.DSControlGrp);
            }
            ds.DSFormatURI = formatURI;
            ds.DatastreamAltIDs = altIDs;
            ds.DSMIME = MIMEType;
            ds.DSChecksumType = Datastream.validateChecksumType(checksumType);

            if (checksum != null && checksumType != null) {
                String check = ds.getChecksum();
                if (!checksum.equals(check)) {
                    throw new ValidationException("Checksum Mismatch: " + check);
                }
            }

            // Update audit trail
            Date nowUTC = Server.getCurrentDate(context);
            addAuditRecord(context, w, "addDatastream", ds.DatastreamID, logMessage, nowUTC);

            // Commit the updates
            ds.DSCreateDT = nowUTC;
            w.addDatastream(ds, true);
            w.commit("Added a new datastream");

            return ds.DatastreamID;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed addDatastream(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", dsID: ").append(dsID);
                appendAltIDs(logMsg, altIDs);
                logMsg.append(", dsLabel: ").append(dsLabel);
                logMsg.append(", versionable: ").append(versionable);
                logMsg.append(", MIMEType: ").append(MIMEType);
                logMsg.append(", formatURI: ").append(formatURI);
                logMsg.append(", dsLocation: ").append(dsLocation);
                logMsg.append(", controlGroup: ").append(controlGroup);
                logMsg.append(", dsState: ").append(dsState);
                logMsg.append(", checksumType: ").append(checksumType);
                logMsg.append(", checksum: ").append(checksum);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "addDatastream");
        }
    }

    public Date modifyDatastreamByReference(Context context, String pid, String datastreamId, String[] altIDs,
            String dsLabel, String mimeType, String formatURI, String dsLocation, String checksumType,
            String checksum, String logMessage, boolean force) throws ServerException {

        // check for valid xml name for datastream ID
        if (datastreamId != null) {
            if (!XMLUtils.isWellFormedXMLName(datastreamId)) {
                throw new InvalidXMLNameException("Invalid syntax for " + "datastream ID. The datastream ID of \""
                        + datastreamId + "\" is not a valid XML Name");
            }
        }

        if (datastreamId.equals("AUDIT") || datastreamId.equals("FEDORA-AUDITTRAIL")) {
            throw new GeneralException(
                    "Modification of the system-controlled AUDIT" + " datastream is not permitted.");
        }

        DOWriter w = null;
        try {
            LOG.debug("Entered modifyDatastreamByReference");
            m_authz.enforceModifyDatastreamByReference(context, pid, datastreamId, altIDs, mimeType, formatURI,
                    dsLocation, checksumType, checksum);

            checkDatastreamLabel(dsLabel);
            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            fedora.server.storage.types.Datastream orig = w.GetDatastream(datastreamId, null);
            Date nowUTC; // variable for ds modified date

            // some forbidden scenarios...
            if (orig.DSControlGrp.equals("X")) {
                throw new GeneralException("Inline XML datastreams must be modified by value, not by reference.");
            }
            if (orig.DSState.equals("D")) {
                throw new GeneralException("Changing attributes on deleted datastreams is forbidden.");
            }

            // A NULL INPUT PARM MEANS NO CHANGE TO DS ATTRIBUTE...
            // if input parms are null, the ds attribute should not be changed,
            // so set the parm values to the existing values in the datastream.
            if (dsLabel == null) {
                dsLabel = orig.DSLabel;
            }
            if (mimeType == null) {
                mimeType = orig.DSMIME;
            }
            if (formatURI == null) {
                formatURI = orig.DSFormatURI;
            }
            if (altIDs == null) {
                altIDs = orig.DatastreamAltIDs;
            }
            if (checksumType == null) {
                checksumType = orig.DSChecksumType;
            } else {
                checksumType = Datastream.validateChecksumType(checksumType);
            }

            // In cases where an empty attribute value is not allowed, then
            // NULL or EMPTY PARM means no change to ds attribute...
            if (dsLocation == null || dsLocation.equals("")) {
                if (orig.DSControlGrp.equals("M")) {
                    // if managed content location is unspecified,
                    // cause a copy of the prior content to be made at
                    // commit-time
                    dsLocation = DatastreamManagedContent.COPY_SCHEME + orig.DSLocation;
                } else {
                    dsLocation = orig.DSLocation;
                }
            } else {
                ValidationUtility.validateURL(dsLocation, orig.DSControlGrp);
            }

            // if "force" is false and the mime type changed, validate the
            // original datastream with respect to any disseminators it is
            // involved in, and keep a record of that information for later
            // (so we can determine whether the mime type change would cause
            // data contract invalidation)
            // Map oldValidationReports = null;
            // if ( !mimeType.equals(orig.DSMIME) && !force) {
            // oldValidationReports = getAllBindingMapValidationReports(
            // context, w, datastreamId);
            // }

            // instantiate the right class of datastream
            // (inline xml "X" datastreams have already been rejected)
            Datastream newds;
            if (orig.DSControlGrp.equals("M")) {
                newds = new DatastreamManagedContent();
            } else {
                newds = new DatastreamReferencedContent();
            }
            // update ds attributes that are common to all versions...
            // first, those that cannot be changed by client...
            newds.DatastreamID = orig.DatastreamID;
            newds.DSControlGrp = orig.DSControlGrp;
            newds.DSInfoType = orig.DSInfoType;
            // next, those that can be changed by client...
            newds.DSState = orig.DSState;
            newds.DSVersionable = orig.DSVersionable;

            // update ds version-level attributes, and
            // make sure ds gets a new version id
            newds.DSVersionID = w.newDatastreamID(datastreamId);
            newds.DSLabel = dsLabel;
            newds.DSMIME = mimeType;
            newds.DSFormatURI = formatURI;
            newds.DatastreamAltIDs = altIDs;
            nowUTC = Server.getCurrentDate(context);
            newds.DSCreateDT = nowUTC;
            // newds.DSSize will be computed later
            newds.DSLocation = dsLocation;
            newds.DSChecksumType = checksumType;

            // next, add the datastream via the object writer
            w.addDatastream(newds, orig.DSVersionable);

            // if a checksum is passed in verify that the checksum computed for
            // the datastream
            // matches the one that is passed in.
            if (checksum != null) {
                if (checksumType == null) {
                    newds.DSChecksumType = orig.DSChecksumType;
                }
                String check = newds.getChecksum();
                if (!checksum.equals(check)) {
                    throw new ValidationException("Checksum Mismatch: " + check);
                }
            }

            // Update audit trail
            addAuditRecord(context, w, "modifyDatastreamByReference", newds.DatastreamID, logMessage, nowUTC);

            // if all went ok, check if we need to validate, then commit.
            // if (oldValidationReports != null) { // mime changed and
            // force=false
            // rejectMimeChangeIfCausedInvalidation(
            // oldValidationReports,
            // getAllBindingMapValidationReports(context,
            // w,
            // datastreamId));
            // }
            w.commit(logMessage);

            return nowUTC;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed modifyDatastreamByReference(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamId: ").append(datastreamId);
                appendAltIDs(logMsg, altIDs);
                logMsg.append(", dsLabel: ").append(dsLabel);
                logMsg.append(", mimeType: ").append(mimeType);
                logMsg.append(", formatURI: ").append(formatURI);
                logMsg.append(", dsLocation: ").append(dsLocation);
                logMsg.append(", checksumType: ").append(checksumType);
                logMsg.append(", checksum: ").append(checksum);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(", force: ").append(force);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "modifyDatastreamByReference");
        }
    }

    public Date modifyDatastreamByValue(Context context, String pid, String datastreamId, String[] altIDs,
            String dsLabel, String mimeType, String formatURI, InputStream dsContent, String checksumType,
            String checksum, String logMessage, boolean force) throws ServerException {

        // check for valid xml name for datastream ID
        if (datastreamId != null) {
            if (!XMLUtils.isWellFormedXMLName(datastreamId)) {
                throw new InvalidXMLNameException("Invalid syntax for " + "datastream ID. The datastream ID of \""
                        + datastreamId + "\" is not a valid XML Name");
            }
        }

        if (datastreamId.equals("AUDIT") || datastreamId.equals("FEDORA-AUDITTRAIL")) {
            throw new GeneralException(
                    "Modification of the system-controlled AUDIT" + " datastream is not permitted.");
        }
        DOWriter w = null;
        try {
            LOG.debug("Entered modifyDatastreamByValue");
            m_authz.enforceModifyDatastreamByValue(context, pid, datastreamId, altIDs, mimeType, formatURI,
                    checksumType, checksum);

            checkDatastreamLabel(dsLabel);
            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            fedora.server.storage.types.Datastream orig = w.GetDatastream(datastreamId, null);

            // some forbidden scenarios...
            if (orig.DSState.equals("D")) {
                throw new GeneralException("Changing attributes on deleted datastreams is forbidden.");
            }
            if (!orig.DSControlGrp.equals("X")) {
                throw new GeneralException("Only content of inline XML datastreams may" + " be modified by value.\n"
                        + "Use modifyDatastreamByReference instead.");
            }

            // A NULL INPUT PARM MEANS NO CHANGE TO DS ATTRIBUTE...
            // if input parms are null, the ds attribute should not be changed,
            // so set the parm values to the existing values in the datastream.
            if (dsLabel == null) {
                dsLabel = orig.DSLabel;
            }
            if (mimeType == null) {
                mimeType = orig.DSMIME;
            }
            if (formatURI == null) {
                formatURI = orig.DSFormatURI;
            }
            if (altIDs == null) {
                altIDs = orig.DatastreamAltIDs;
            }
            if (checksumType == null) {
                checksumType = orig.DSChecksumType;
            } else {
                checksumType = Datastream.validateChecksumType(checksumType);
            }

            DatastreamXMLMetadata newds = new DatastreamXMLMetadata();
            newds.DSMDClass = ((DatastreamXMLMetadata) orig).DSMDClass;
            if (dsContent == null) {
                // If the dsContent input stream parm is null,
                // that means "do not change the content".
                // Accordingly, here we just make a copy of the old content.
                newds.xmlContent = ((DatastreamXMLMetadata) orig).xmlContent;
            } else {
                // set and validate the content
                newds.xmlContent = getEmbeddableXML(dsContent);
                ValidationUtility.validateReservedDatastream(PID.getInstance(pid), orig.DatastreamID,
                        newds.getContentStream());
            }

            // update ds attributes that are common to all versions...
            // first, those that cannot be changed by client...
            newds.DatastreamID = orig.DatastreamID;
            newds.DSControlGrp = orig.DSControlGrp;
            newds.DSInfoType = orig.DSInfoType;
            // next, those that can be changed by client...
            newds.DSState = orig.DSState;
            newds.DSVersionable = orig.DSVersionable;

            // update ds version level attributes, and
            // make sure ds gets a new version id
            newds.DSVersionID = w.newDatastreamID(datastreamId);
            newds.DSLabel = dsLabel;
            newds.DatastreamAltIDs = altIDs;
            newds.DSMIME = mimeType;
            newds.DSFormatURI = formatURI;
            Date nowUTC = Server.getCurrentDate(context);
            newds.DSCreateDT = nowUTC;

            newds.DSChecksumType = checksumType;

            // next, add the datastream via the object writer
            w.addDatastream(newds, orig.DSVersionable);

            // if a checksum is passed in verify that the checksum computed for
            // the datastream
            // matches the one that is passed in.
            if (checksum != null) {
                String check = newds.getChecksum();
                if (!checksum.equals(check)) {
                    throw new ValidationException("Checksum Mismatch: " + check);
                }
            }

            // Update audit trail
            addAuditRecord(context, w, "modifyDatastreamByValue", newds.DatastreamID, logMessage, nowUTC);

            w.commit(logMessage);

            return nowUTC;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed modifyDatastreamByValue(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamId: ").append(datastreamId);
                appendAltIDs(logMsg, altIDs);
                logMsg.append(", dsLabel: ").append(dsLabel);
                logMsg.append(", mimeType: ").append(mimeType);
                logMsg.append(", formatURI: ").append(formatURI);
                logMsg.append(", dsContent ");
                logMsg.append(", checksumType: ").append(checksumType);
                logMsg.append(", checksum: ").append(checksum);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(", force: ").append(force);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "modifyDatastreamByValue");
        }
    }

    public Date[] purgeDatastream(Context context, String pid, String datastreamID, Date startDT, Date endDT,
            String logMessage, boolean force) throws ServerException {
        if (force) {
            throw new GeneralException("Forced datastream removal is not " + "yet supported.");
        }
        DOWriter w = null;
        try {
            LOG.debug("Entered purgeDatastream");

            m_authz.enforcePurgeDatastream(context, pid, datastreamID, endDT);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            Date[] deletedDates = w.removeDatastream(datastreamID, startDT, endDT);
            // check if there's at least one version with this id...
            if (w.GetDatastream(datastreamID, null) == null) {
                // if deleting would result in no versions remaining,
                // only continue if there are no disseminators that use
                // this datastream.
                // to do this, we must look through all versions of every
                // disseminator, regardless of state
                ArrayList<String> usedList = new ArrayList<String>();
                if (datastreamID.equals("DC")) {
                    usedList.add("The default disseminator");
                }
                if (usedList.size() > 0) {
                    StringBuffer msg = new StringBuffer();
                    msg.append("Cannot purge entire datastream because it\n");
                    msg.append("is used by the following disseminators:");
                    for (int i = 0; i < usedList.size(); i++) {
                        msg.append("\n - " + usedList.get(i));
                    }
                    throw new GeneralException(msg.toString());
                }
            }
            // add an explanation of what happened to the user-supplied message.
            if (logMessage == null) {
                logMessage = "";
            } else {
                logMessage += " . . . ";
            }
            logMessage += getPurgeLogMessage("datastream", datastreamID, startDT, endDT, deletedDates);

            // Update audit trail
            Date nowUTC = Server.getCurrentDate(context);
            addAuditRecord(context, w, "purgeDatastream", datastreamID, logMessage, nowUTC);

            // It looks like all went ok, so commit
            w.commit(logMessage);
            // ... then give the response
            return deletedDates;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed purgeDatastream(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamID: ").append(datastreamID);
                logMsg.append(", startDT: ").append(startDT);
                logMsg.append(", endDT: ").append(endDT);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "purgeDatastream");
        }
    }

    private String getPurgeLogMessage(String kindaThing, String id, Date start, Date end, Date[] deletedDates) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        StringBuffer buf = new StringBuffer();
        buf.append("Purged ");
        buf.append(kindaThing);
        buf.append(" (ID=");
        buf.append(id);
        buf.append("), versions ranging from ");
        if (start == null) {
            buf.append("the beginning of time");
        } else {
            buf.append(formatter.format(start));
        }
        buf.append(" to ");
        if (end == null) {
            buf.append("the end of time");
        } else {
            buf.append(formatter.format(end));
        }
        buf.append(".  This resulted in the permanent removal of ");
        buf.append(deletedDates.length + " ");
        buf.append(kindaThing);
        buf.append(" version(s) (");
        for (int i = 0; i < deletedDates.length; i++) {
            if (i > 0) {
                buf.append(", ");
            }
            buf.append(formatter.format(deletedDates[i]));
        }
        buf.append(") and all associated audit records.");
        return buf.toString();
    }

    public Datastream getDatastream(Context context, String pid, String datastreamID, Date asOfDateTime)
            throws ServerException {
        try {
            LOG.debug("Entered getDatastream");

            m_authz.enforceGetDatastream(context, pid, datastreamID, asOfDateTime);

            DOReader r = m_manager.getReader(Server.GLOBAL_CHOICE, context, pid);

            return r.GetDatastream(datastreamID, asOfDateTime);
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed getDatastream(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamID: ").append(datastreamID);
                logMsg.append(", asOfDateTime: ").append(asOfDateTime);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting getDatastream");
        }
    }

    public Datastream[] getDatastreams(Context context, String pid, Date asOfDateTime, String state)
            throws ServerException {
        try {
            LOG.debug("Entered getDatastreams");

            m_authz.enforceGetDatastreams(context, pid, asOfDateTime, state);

            DOReader r = m_manager.getReader(Server.GLOBAL_CHOICE, context, pid);

            return r.GetDatastreams(asOfDateTime, state);
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed getDatastreams(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", asOfDateTime: ").append(asOfDateTime);
                logMsg.append(", state: ").append(state);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting getDatastreams");
        }
    }

    public Datastream[] getDatastreamHistory(Context context, String pid, String datastreamID)
            throws ServerException {
        try {
            LOG.debug("Entered getDatastreamHistory");

            m_authz.enforceGetDatastreamHistory(context, pid, datastreamID);

            DOReader r = m_manager.getReader(Server.GLOBAL_CHOICE, context, pid);
            Date[] versionDates = r.getDatastreamVersions(datastreamID);
            Datastream[] versions = new Datastream[versionDates.length];
            for (int i = 0; i < versionDates.length; i++) {
                versions[i] = r.GetDatastream(datastreamID, versionDates[i]);
            }
            // sort, ascending
            Arrays.sort(versions, new DatastreamDateComparator());
            // reverse it (make it descend, so most recent date is element 0)
            Datastream[] out = new Datastream[versions.length];
            for (int i = 0; i < versions.length; i++) {
                out[i] = versions[versions.length - 1 - i];
            }

            return out;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed getDatastreamHistory(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamID: ").append(datastreamID);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting getDatastreamHistory");
        }
    }

    public class DatastreamDateComparator implements Comparator<Object> {

        public int compare(Object o1, Object o2) {
            long ms1 = ((Datastream) o1).DSCreateDT.getTime();
            long ms2 = ((Datastream) o1).DSCreateDT.getTime();
            if (ms1 < ms2) {
                return -1;
            }
            if (ms1 > ms2) {
                return 1;
            }
            return 0;
        }
    }

    public String[] getNextPID(Context context, int numPIDs, String namespace) throws ServerException {
        try {
            LOG.debug("Entered getNextPID");
            m_authz.enforceGetNextPid(context, namespace, numPIDs);

            String[] pidList = null;

            // If the pidList is in the RecoveryContext, just reserve them
            // rather than generating new ones.
            if (context instanceof RecoveryContext) {
                RecoveryContext rContext = (RecoveryContext) context;
                pidList = rContext.getRecoveryValues(Constants.RECOVERY.PID_LIST.uri);
                if (pidList != null && pidList.length > 0) {
                    LOG.debug("Reserving and returning PID_LIST " + "from recovery context");
                    m_manager.reservePIDs(pidList);
                }
            }

            if (pidList == null || pidList.length == 0) {
                pidList = m_manager.getNextPID(numPIDs, namespace);
            }

            return pidList;

        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed getNextPID(");
                logMsg.append("numPIDs: ").append(numPIDs);
                logMsg.append(", namespace: ").append(namespace);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting getNextPID");
        }
    }

    public String putTempStream(Context context, InputStream in) throws StreamWriteException, AuthzException {
        m_authz.enforceUpload(context);
        // first clean up after old stuff
        purgeUploadedFiles();
        // then generate an id
        int id = getNextTempId(context);
        // and attempt to save the stream
        File outFile = new File(m_tempDir, "" + id);
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(outFile);
            StreamUtility.pipeStream(in, out, 32768);
        } catch (Exception e) {
            if (out != null) {
                try {
                    out.close();
                } catch (Exception ex) {
                }
                outFile.delete();
            }
            throw new StreamWriteException("Error writing temp stream", e);
        }
        // if we got this far w/o an exception, add to hash with current time
        // and return the identifier-that-looks-like-a-url
        long now = System.currentTimeMillis();
        m_uploadStartTime.put("" + id, new Long(now));
        return "uploaded://" + id;
    }

    private synchronized int getNextTempId(Context context) {

        int recoveryId = -1;

        // If the RecoveryContext has an uploaded://n url, use n.
        if (context instanceof RecoveryContext) {
            RecoveryContext rContext = (RecoveryContext) context;
            String uploadURL = rContext.getRecoveryValue(Constants.RECOVERY.UPLOAD_ID.uri);
            if (uploadURL != null) {
                try {
                    String n = uploadURL.substring(11);
                    recoveryId = Integer.parseInt(n);
                } catch (Exception e) {
                    throw new IllegalArgumentException(
                            "Unable to parse UPLOAD_ID " + "from recovery context: '" + uploadURL + "'");
                }
            }
        }
        if (recoveryId == -1) {
            m_lastId++;
        } else {
            m_lastId = recoveryId;
        }
        return m_lastId;
    }

    public InputStream getTempStream(String id) throws StreamReadException {
        // it should come in starting with "uploaded://"
        if (id.startsWith(DatastreamManagedContent.UPLOADED_SCHEME) || id.length() < 12) {
            String internalId = id.substring(11);
            if (m_uploadStartTime.get(internalId) != null) {
                // found... return inputstream
                try {
                    return new FileInputStream(new File(m_tempDir, internalId));
                } catch (Exception e) {
                    throw new StreamReadException(e.getMessage());
                }
            } else {
                throw new StreamReadException("Id specified, '" + id + "', does not match an existing file.");
            }
        } else {
            throw new StreamReadException("Invalid id syntax '" + id + "'.");
        }
    }

    public Date setDatastreamState(Context context, String pid, String datastreamID, String dsState,
            String logMessage) throws ServerException {
        DOWriter w = null;
        try {
            LOG.debug("Entered setDatastreamState");

            m_authz.enforceSetDatastreamState(context, pid, datastreamID, dsState);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            if (!dsState.equals("A") && !dsState.equals("D") && !dsState.equals("I")) {
                throw new InvalidStateException(
                        "The datastream state of \"" + dsState + "\" is invalid. The allowed values for state are: "
                                + " A (active), D (deleted), and I (inactive).");
            }
            w.setDatastreamState(datastreamID, dsState);

            // Update audit trail
            Date nowUTC = Server.getCurrentDate(context);
            addAuditRecord(context, w, "setDatastreamState", datastreamID, logMessage, nowUTC);

            // if all went ok, commit
            w.commit(logMessage);
            return nowUTC;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed setDatastreamState(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamID: ").append(datastreamID);
                logMsg.append(", dsState: ").append(dsState);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "setDatastreamState");
        }
    }

    public Date setDatastreamVersionable(Context context, String pid, String datastreamID, boolean versionable,
            String logMessage) throws ServerException {
        DOWriter w = null;
        try {
            LOG.debug("Entered setDatastreamVersionable");

            m_authz.enforceSetDatastreamVersionable(context, pid, datastreamID, versionable);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            w.setDatastreamVersionable(datastreamID, versionable);

            // Update audit trail
            Date nowUTC = Server.getCurrentDate(context);
            addAuditRecord(context, w, "setDatastreamVersionable", datastreamID, logMessage, nowUTC);

            // if all went ok, commit
            w.commit(logMessage);
            return nowUTC;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed setDatastreamVersionable(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamID: ").append(datastreamID);
                logMsg.append(", versionable: ").append(versionable);
                logMsg.append(", logMessage: ").append(logMessage);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "setDatastreamVersionable");
        }
    }

    public String compareDatastreamChecksum(Context context, String pid, String datastreamID, Date versionDate)
            throws ServerException {
        DOReader r = null;
        try {
            LOG.debug("Entered compareDatastreamChecksum");

            m_authz.enforceCompareDatastreamChecksum(context, pid, datastreamID, versionDate);

            LOG.debug("Getting Reader");
            r = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, pid);
            LOG.debug("Getting datastream:" + datastreamID + "date: " + versionDate);
            Datastream ds = r.GetDatastream(datastreamID, versionDate);
            LOG.debug("Got Datastream, comparing checksum");
            boolean check = ds.compareChecksum();
            LOG.debug("compared checksum = " + check);

            return check ? ds.getChecksum() : "Checksum validation error";
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed compareDatastreamChecksum(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", datastreamID: ").append(datastreamID);
                logMsg.append(", versionDate: ").append(versionDate);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting compareDatastreamChecksum");
        }
    }

    /**
     * Get a byte array containing an xml chunk that is safe to embed in another
     * UTF-8 xml document.
     * <p>
     * This will ensure that the xml is:
     * <ul>
     * <li> well-formed. If not, an exception will be raised.</li>
     * <li> encoded in UTF-8. It will be converted otherwise.</li>
     * <li> devoid of processing instructions. These will be stripped if
     * present.</li>
     * <li> devoid of DOCTYPE declarations. These will be stripped if present.</li>
     * <li> devoid of internal entity references. These will be expanded if
     * present.</li>
     * </ul>
     * </p>
     */
    private byte[] getEmbeddableXML(InputStream in) throws GeneralException {
        // parse with xerces and re-serialize the fixed xml to a byte array
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            OutputFormat fmt = new OutputFormat("XML", "UTF-8", true);
            fmt.setIndent(2);
            fmt.setLineWidth(120);
            fmt.setPreserveSpace(false);
            fmt.setOmitXMLDeclaration(true);
            fmt.setOmitDocumentType(true);
            XMLSerializer ser = new XMLSerializer(out, fmt);
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(in);
            ser.serialize(doc);
            return out.toByteArray();
        } catch (Exception e) {
            String message = e.getMessage();
            if (message == null) {
                message = "";
            }
            throw new GeneralException("XML was not well-formed. " + message, e);
        }
    }

    private void checkDatastreamID(String id) throws ValidationException {
        checkString(id, "Datastream id", ValidationConstants.DATASTREAM_ID_MAXLEN,
                ValidationConstants.DATASTREAM_ID_BADCHARS);
    }

    private void checkDatastreamLabel(String label) throws ValidationException {
        checkString(label, "Datastream label", ValidationConstants.DATASTREAM_LABEL_MAXLEN, null);
    }

    private void checkObjectLabel(String label) throws ValidationException {
        checkString(label, "Object label", ValidationConstants.OBJECT_LABEL_MAXLEN, null);
    }

    private void checkString(String string, String kind, int maxLen, char[] badChars) throws ValidationException {
        if (string != null) {
            if (string.length() > maxLen) {
                throw new ValidationException(
                        kind + " is too long. Maximum " + "length is " + maxLen + " characters.");
            } else if (badChars != null) {
                for (char c : badChars) {
                    if (string.indexOf(c) != -1) {
                        throw new ValidationException(
                                kind + " contains a " + "'" + c + "', but that character is not " + "allowed.");
                    }
                }
            }
        }
    }

    // helper class to get pid from subject and to get URI form of subject
    // subject can either be a pid or an info:fedora/ uri
    private static class SubjectProcessor {

        private static Pattern pidRegex = Pattern
                .compile("^([A-Za-z0-9]|-|\\.)+:(([A-Za-z0-9])|-|\\.|~|_|(%[0-9A-F]{2}))+$");

        static String getSubjectAsUri(String subject) {
            // if we weren't given a pid, assume it's a URI
            if (!isPid(subject))
                return subject;
            // otherwise return URI from the pid
            LOG.warn("Relationships API methods:  the 'pid' (" + subject
                    + ") form of a relationship's subject is deprecated.  Please specify the subject using the "
                    + Constants.FEDORA.uri + " uri scheme.");
            return PID.toURI(subject);
        }

        static String getSubjectPID(String subject) throws ServerException {
            if (isPid(subject))
                return subject;
            // check for info:uri scheme
            if (subject.startsWith(Constants.FEDORA.uri)) {
                // pid is everything after the first / to the 2nd / or to the end of the string
                return subject.split("/", 3)[1];

            } else {
                throw new GeneralException("Subject URI must be in the " + Constants.FEDORA.uri + " scheme.");
            }

        }

        private static boolean isPid(String subject) {
            return pidRegex.matcher(subject).matches();
        }
    }

    public RelationshipTuple[] getRelationships(Context context, String subject, String relationship)
            throws ServerException {
        DOReader r = null;
        String pid = null;
        try {
            LOG.debug("Entered getRelationships");

            pid = SubjectProcessor.getSubjectPID(subject);

            m_authz.enforceGetRelationships(context, pid, relationship);

            r = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, pid);
            LOG.debug("Getting Relationships:  pid = " + pid + " predicate = " + relationship);
            try {
                URIReference pred = null;
                if (relationship != null) {
                    pred = new SimpleURIReference(new URI(relationship));
                }
                URIReference subj = null;
                if (subject != null) {
                    subj = new SimpleURIReference(new URI(SubjectProcessor.getSubjectAsUri(subject)));
                }
                Set<RelationshipTuple> tuples = r.getRelationships(subj, pred, null);
                return tuples.toArray(new RelationshipTuple[tuples.size()]);
            } catch (URISyntaxException e) {
                throw new GeneralException("Relationship must be a URI", e);
            }
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed getRelationships(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", relationship: ").append(relationship);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            LOG.debug("Exiting getRelationships");
        }
    }

    public boolean addRelationship(Context context, String subject, String relationship, String object,
            boolean isLiteral, String datatype) throws ServerException {
        DOWriter w = null;
        String pid = null;
        try {
            LOG.debug("Entered addRelationship");
            pid = SubjectProcessor.getSubjectPID(subject);
            m_authz.enforceAddRelationship(context, pid, relationship, object, isLiteral, datatype);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            boolean added = w.addRelationship(SubjectProcessor.getSubjectAsUri(subject), relationship, object,
                    isLiteral, datatype);

            // if all went ok, commit
            if (added) {
                w.commit(null);
            }

            return added;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed addRelationship(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", relationship: ").append(relationship);
                logMsg.append(", object: ").append(object);
                logMsg.append(", isLiteral: ").append(isLiteral);
                logMsg.append(", datatype: ").append(datatype);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "addRelationship");
        }
    }

    public boolean purgeRelationship(Context context, String subject, String relationship, String object,
            boolean isLiteral, String datatype) throws ServerException {
        DOWriter w = null;
        String pid = null;
        try {
            LOG.debug("Entered purgeRelationship");
            pid = SubjectProcessor.getSubjectPID(subject);
            m_authz.enforcePurgeRelationship(context, pid, relationship, object, isLiteral, datatype);

            w = m_manager.getWriter(Server.USE_DEFINITIVE_STORE, context, pid);
            boolean purged = w.purgeRelationship(SubjectProcessor.getSubjectAsUri(subject), relationship, object,
                    isLiteral, datatype);

            // if all went ok, commit
            if (purged) {
                w.commit(null);
            }
            return purged;
        } finally {
            // Log completion
            if (LOG.isInfoEnabled()) {
                StringBuilder logMsg = new StringBuilder("Completed purgeRelationship(");
                logMsg.append("pid: ").append(pid);
                logMsg.append(", relationship: ").append(relationship);
                logMsg.append(", object: ").append(object);
                logMsg.append(", isLiteral: ").append(isLiteral);
                logMsg.append(", datatype: ").append(datatype);
                logMsg.append(")");
                LOG.info(logMsg.toString());
            }

            finishModification(w, "purgeRelationship");
        }
    }

    /**
     * Creates a new audit record and adds it to the digital object audit trail.
     */
    private void addAuditRecord(Context context, DOWriter w, String action, String componentID,
            String justification, Date nowUTC) throws ServerException {
        AuditRecord audit = new AuditRecord();
        audit.id = w.newAuditRecordID();
        audit.processType = "Fedora API-M";
        audit.action = action;
        audit.componentID = componentID;
        audit.responsibility = context.getSubjectValue(Constants.SUBJECT.LOGIN_ID.uri);
        audit.date = nowUTC;
        audit.justification = justification;
        w.getAuditRecords().add(audit);
    }

    /**
     * Appends alt IDs to the log message.
     */
    private static void appendAltIDs(StringBuilder logMsg, String[] altIDs) {
        logMsg.append(", altIDs: ");
        if (altIDs == null) {
            logMsg.append("null");
        } else {
            for (String altID : altIDs) {
                logMsg.append("'").append(altID).append("'");
            }
        }
    }

    /**
     * Deletes expired uploaded files.
     * <p>
     * This method is called for each upload. But we respect a minimim delay
     * between two purges. This delay is given by m_purgeDelayInMillis.
     */
    private void purgeUploadedFiles() {
        long currentTimeMillis = System.currentTimeMillis();

        // Do purge if purge delay is past before last purge
        // -------------------------------------------------
        long nextPurgeInMillis = this.m_lastPurgeInMillis + this.m_purgeDelayInMillis;
        if (nextPurgeInMillis < currentTimeMillis) {
            this.m_lastPurgeInMillis = currentTimeMillis;

            // Compute limit file time to purged
            // ---------------------------------
            long minStartTime = currentTimeMillis - (this.m_uploadStorageMinutes * 60000);

            // List files to purge and remove filename to map
            // This operation is synchronized to be thread-safe
            // ------------------------------------------------
            List<String> removeList = new ArrayList<String>();
            synchronized (this.m_uploadStartTime) {
                for (Entry<String, Long> entry : m_uploadStartTime.entrySet()) {
                    String filename = entry.getKey();
                    long startTime = entry.getValue().longValue();
                    if (startTime < minStartTime) {
                        removeList.add(filename);
                    }
                }
                for (String filename : removeList) {
                    this.m_uploadStartTime.remove(filename);
                }
            }

            // Delete file to purged
            // This operation is out of synchronised block for performances
            // ------------------------------------------------------------
            for (int i = 0; i < removeList.size(); i++) {
                String id = removeList.get(i);

                File file = new File(this.m_tempDir, id);
                if (file.exists()) {
                    if (file.delete()) {
                        LOG.info("Removed uploaded file '" + id + "' because it expired.");
                    } else {
                        LOG.warn("Could not remove expired uploaded file '" + id
                                + "'. Check permissions in management/upload/ directory.");
                    }
                }
            }
        }
    }

}