de.escidoc.core.common.business.fedora.Utility.java Source code

Java tutorial

Introduction

Here is the source code for de.escidoc.core.common.business.fedora.Utility.java

Source

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the Common Development and Distribution License, Version 1.0
 * only (the "License"). You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at license/ESCIDOC.LICENSE or http://www.escidoc.de/license. See the License for
 * the specific language governing permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each file and include the License file at
 * license/ESCIDOC.LICENSE. If applicable, add the following below this CDDL HEADER, with the fields enclosed by
 * brackets "[]" replaced with your own identifying information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 * Copyright 2006-2011 Fachinformationszentrum Karlsruhe Gesellschaft fuer wissenschaftlich-technische Information mbH
 * and Max-Planck-Gesellschaft zur Foerderung der Wissenschaft e.V. All rights reserved. Use is subject to license
 * terms.
 */

package de.escidoc.core.common.business.fedora;

import de.escidoc.core.common.business.Constants;
import de.escidoc.core.common.business.PropertyMapKeys;
import de.escidoc.core.common.business.fedora.datastream.Datastream;
import de.escidoc.core.common.business.fedora.resources.Container;
import de.escidoc.core.common.business.fedora.resources.Item;
import de.escidoc.core.common.business.fedora.resources.Relation;
import de.escidoc.core.common.business.fedora.resources.cmm.ContentModel;
import de.escidoc.core.common.business.fedora.resources.interfaces.FedoraResource;
import de.escidoc.core.common.business.fedora.resources.interfaces.VersionableResource;
import de.escidoc.core.common.exceptions.application.invalid.InvalidContentException;
import de.escidoc.core.common.exceptions.application.invalid.InvalidContextException;
import de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException;
import de.escidoc.core.common.exceptions.application.missing.MissingMethodParameterException;
import de.escidoc.core.common.exceptions.application.notfound.ComponentNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.ContainerNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.ContentModelNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.ContentRelationNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.ContextNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.ItemNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.OrganizationalUnitNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.ResourceNotFoundException;
import de.escidoc.core.common.exceptions.application.notfound.StreamNotFoundException;
import de.escidoc.core.common.exceptions.application.violated.LockingException;
import de.escidoc.core.common.exceptions.application.violated.OptimisticLockingException;
import de.escidoc.core.common.exceptions.system.EncodingSystemException;
import de.escidoc.core.common.exceptions.system.FedoraSystemException;
import de.escidoc.core.common.exceptions.system.FileSystemException;
import de.escidoc.core.common.exceptions.system.IntegritySystemException;
import de.escidoc.core.common.exceptions.system.SystemException;
import de.escidoc.core.common.exceptions.system.TripleStoreSystemException;
import de.escidoc.core.common.exceptions.system.WebserverSystemException;
import de.escidoc.core.common.exceptions.system.XmlParserSystemException;
import de.escidoc.core.common.util.configuration.EscidocConfiguration;
import de.escidoc.core.common.util.service.UserContext;
import de.escidoc.core.common.util.stax.StaxParser;
import de.escidoc.core.common.util.stax.handler.AddNewSubTreesToDatastream;
import de.escidoc.core.common.util.stax.handler.ItemRelsExtUpdateHandler;
import de.escidoc.core.common.util.stax.handler.MultipleExtractor;
import de.escidoc.core.common.util.string.StringUtility;
import de.escidoc.core.common.util.xml.Elements;
import de.escidoc.core.common.util.xml.XmlUtility;
import de.escidoc.core.common.util.xml.factory.CommonFoXmlProvider;
import de.escidoc.core.common.util.xml.factory.XmlTemplateProviderConstants;
import de.escidoc.core.common.util.xml.stax.events.Attribute;
import de.escidoc.core.common.util.xml.stax.events.StartElement;
import de.escidoc.core.common.util.xml.stax.events.StartElementWithChildElements;
import de.escidoc.core.st.service.interfaces.StagingFileHandlerInterface;
import org.apache.xpath.XPathAPI;
import org.escidoc.core.utils.io.EscidocBinaryContent;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.TransformerException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Some utilities.
 *
 * @author Michael Schneider
 */
@Service("business.Utility")
public class Utility {

    private static final String RESOURCE_BASE_URL = "resourceBaseUrl";

    /**
     * The pattern used to extract the redirect base url and path from the staging file XML representation.
     */
    private static final Pattern REDIRECT_URL_PATTERN = Pattern.compile("xml:base=\"(.*?)\".*xlink:href=\"(.*?)\"");

    @Autowired
    @Qualifier("service.StagingFileHandler")
    private StagingFileHandlerInterface stagingFileHandler;

    @Autowired
    @Qualifier("business.TripleStoreUtility")
    private TripleStoreUtility tripleStoreUtility;

    /**
     * Protected constructor to prevent instantiation outside of the Spring-context.
     */
    protected Utility() {
    }

    /**
     * Fetches the id from the link. It is the String after the last '/' in the link.
     *
     * @param link The link
     * @return The extracted id.
     */
    public static String getId(final String link) {
        String result = link;
        final int index = link.lastIndexOf('/');
        if (index != -1) {
            result = link.substring(link.lastIndexOf('/') + 1);
        }
        return result;
    }

    /**
     * Checks if the given dates are equal. If so the change is permitted and true is returned. Otherwise a
     * LockingException is throws.
     *
     * @param fedoraLatestVersionDate The date of the latest version stored in Fedora.
     * @param updateLatestVersionDate The date of the version the application retrieved and wants to update.
     * @param label                   A label to identify the object to change.
     * @throws OptimisticLockingException Thrown if a change of the object is not permitted.
     * @throws WebserverSystemException   Thrown in case of an internal error.
     * @throws XmlCorruptedException      Thrown if one of the the given parameters is null
     * @return
     */
    public static boolean checkOptimisticLockingCriteria(final DateTime fedoraLatestVersionDate,
            final DateTime updateLatestVersionDate, final String label)
            throws OptimisticLockingException, XmlCorruptedException {

        if (fedoraLatestVersionDate == null) {
            throw new XmlCorruptedException("last-modification-date of latest version must not be null");
        }
        if (updateLatestVersionDate == null) {
            throw new XmlCorruptedException("last-modification-date of update version must not be null");
        }

        if (!fedoraLatestVersionDate.isEqual((updateLatestVersionDate))) {

            String text = label;
            if (text == null) {
                text = "object to change";
            }

            final String message = "Optimistic locking error! Version of " + text
                    + " does not match most recent version (requested:" + updateLatestVersionDate + " saved:"
                    + fedoraLatestVersionDate + ")! Changes are not permitted.";
            throw new OptimisticLockingException(message);
        }
        return true;
    }

    /**
     * Checks if the given dates are equal. If so the change is permitted and true is returned. Otherwise a
     * LockingException is throws.
     *
     * @param fedoraLatestVersionDate The date of the latest version stored in Fedora.
     * @param updateLatestVersionDate The date of the version the application retrieved and wants to update.
     * @param label                   A label to identify the object to change.
     * @return true if change is permitted
     * @throws OptimisticLockingException Thrown if a change of the object is not permitted.
     * @throws WebserverSystemException   Thrown in case of an internal error.
     * @throws XmlCorruptedException      Thrown if one of the the given parameters is null
     */
    public static boolean checkOptimisticLockingCriteria(final String fedoraLatestVersionDate,
            final String updateLatestVersionDate, final String label)
            throws XmlCorruptedException, OptimisticLockingException {

        if (fedoraLatestVersionDate == null || updateLatestVersionDate == null) {
            throw new XmlCorruptedException("last-modification-date must not be null");
        }

        final DateTime tFedora = new DateTime(fedoraLatestVersionDate);
        final DateTime tUpdate = new DateTime(updateLatestVersionDate);

        return checkOptimisticLockingCriteria(tFedora, tUpdate, label);
    }

    /**
     * Get the id and the name of the current user from the UserContext.
     *
     * @return The the id and the name of the current user. ([0] = id, [1] = name)
     * @throws WebserverSystemException If the current user could not be retrieved.
     */
    public static String[] getCurrentUser() throws WebserverSystemException {

        if (UserContext.getId() == null || UserContext.getRealName() == null) {
            throw new WebserverSystemException("System fault: Current user not set!");
        }
        final String[] result = new String[2];
        result[0] = UserContext.getId();
        result[1] = UserContext.getRealName();

        return result;
    }

    /**
     * Returns true if id of Context of object equals to given contextId.
     *
     * @param id of object to check
     * @param contextId
     * @return true if context of objects equals the parameter contextId.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     */
    public boolean hasSameContext(final String id, final String contextId) throws TripleStoreSystemException {
        final String context = tripleStoreUtility.getContext(id);

        return !(context == null || !context.equals(contextId));
    }

    /**
     * Checks if the given param is not null.
     *
     * @param param The param to check.
     * @param label The label to add to the exception message.
     * @throws MissingMethodParameterException
     *          If the param is null.
     */
    public static void checkNotNull(final Object param, final String label) throws MissingMethodParameterException {
        if (param == null) {
            throw new MissingMethodParameterException(label + " must not be null!");
        }

    }

    /**
     * Checks if a component with id exists.
     *
     * @param id The id of the object.
     * @throws ComponentNotFoundException If the component does not exist or if the object is no component.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     * @throws de.escidoc.core.common.exceptions.system.IntegritySystemException
     */
    public void checkIsComponent(final String id)
            throws ComponentNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.COMPONENT_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ComponentNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if a container with id exists.
     *
     * @param id The id of the object.
     * @throws ContainerNotFoundException If the container does not exist or if the object is no container.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     * @throws de.escidoc.core.common.exceptions.system.IntegritySystemException
     */
    public void checkIsContainer(final String id)
            throws ContainerNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.CONTAINER_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ContainerNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if a context with id exists.
     *
     * @param id The id of the object.
     * @throws ContextNotFoundException If the context does not exist or if the object is no context.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     * @throws de.escidoc.core.common.exceptions.system.IntegritySystemException
     */
    public void checkIsContext(final String id)
            throws ContextNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.CONTEXT_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ContextNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if a Content Relation with id exists.
     *
     * @param id The id of the object.
     * @throws ContentRelationNotFoundException
     *          Thrown if Content Relation the does not exist.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     * @throws de.escidoc.core.common.exceptions.system.IntegritySystemException
     */
    public void checkIsContentRelation(final String id)
            throws ContentRelationNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.CONTENT_RELATION2_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ContentRelationNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if a content model with id exists.
     *
     * @param id The id of the object.
     * @throws ContentModelNotFoundException If the content model does not exist or if the object is no content model.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     * @throws de.escidoc.core.common.exceptions.system.IntegritySystemException
     */
    public void checkIsContentModel(final String id)
            throws ContentModelNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.CONTENT_MODEL_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ContentModelNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if an item with id exists.
     *
     * @param id The id of the object.
     * @throws ItemNotFoundException      If the item does not exist or if the object is no item.
     * @throws WebserverSystemException   Thrown if instance of TripleStore failed.
     * @throws TripleStoreSystemException Thrown in case of Triple Store request or connection failure.
     * @throws IntegritySystemException   Thrown if there is an integrity error with the addressed object.
     */
    public void checkIsItem(final String id)
            throws ItemNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.ITEM_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ItemNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if a relation with id exists.
     *
     * @param id The id.
     * @throws ContentRelationNotFoundException
     *                                    If the relation does not exist or if the object is no relation.
     * @throws WebserverSystemException   Thrown if instance of TripleStore failed.
     * @throws TripleStoreSystemException Thrown in case of Triple Store request or connection failure.
     * @throws IntegritySystemException   Thrown if there is an integrity error with the addressed object.
     */
    public void checkIsRelation(final String id)
            throws ContentRelationNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.RELATION_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new ContentRelationNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Checks if an organizational-unit with id exists.
     *
     * @param id The id of the object.
     * @throws OrganizationalUnitNotFoundException
     *          If the organizational-unit does not exist or if the object is no organizational-unit.
     * @throws de.escidoc.core.common.exceptions.system.TripleStoreSystemException
     * @throws de.escidoc.core.common.exceptions.system.IntegritySystemException
     */
    public void checkIsOrganizationalUnit(final String id)
            throws OrganizationalUnitNotFoundException, TripleStoreSystemException, IntegritySystemException {

        try {
            checkIsOfObjectType(id, Constants.ORGANIZATIONAL_UNIT_OBJECT_TYPE);
        } catch (final ResourceNotFoundException e) {
            throw new OrganizationalUnitNotFoundException(e.getMessage(), e);
        }
    }

    /**
     * Check if the object given by id exists and is of the given type.
     *
     * @param id   The id.
     * @param type The expected type.
     * @throws ResourceNotFoundException  Thrown if resource with provided type does not exist.
     * @throws WebserverSystemException   Thrown if instance of TripleStore failed.
     * @throws TripleStoreSystemException Thrown if request or connection to Triple Store failed.
     * @throws IntegritySystemException   Thrown if no object type is found for an existing object.
     */
    public void checkIsOfObjectType(final String id, final String type)
            throws ResourceNotFoundException, TripleStoreSystemException, IntegritySystemException {

        final String idWithoutVersionNumber = XmlUtility.getObjidWithoutVersion(id);
        final String objectType = tripleStoreUtility.getObjectType(idWithoutVersionNumber);

        if (objectType == null) {
            if (tripleStoreUtility.exists(idWithoutVersionNumber)) {
                // object exists but has no object-type
                throw new IntegritySystemException(
                        StringUtility.format("Object has no object-type ", idWithoutVersionNumber));
            } else {
                throw new ResourceNotFoundException(
                        "Object with id " + idWithoutVersionNumber + " does not exist!");
            }
        } else if (!objectType.equals(type)) {
            throw new ResourceNotFoundException(
                    "Object with id " + idWithoutVersionNumber + " is not a " + type + '!');
        }
    }

    /**
     * Check if the provided object in the xmlData has the same Context id than the object provided by id.
     *
     * @param id      It of the reference object.
     * @param xmlData The XML representation of an object.
     * @throws InvalidContextException Thrown if the contextId of xmlData differs from the ContextId of the reference
     *                                 object.
     * @throws SystemException         Thrown in case of internal failure.
     */
    public void checkSameContext(final String id, final String xmlData)
            throws InvalidContextException, SystemException {

        // TODO This is a quick fix hack. Change to StAX parser. And, may be,
        // move the check to another level in create to avoid a double XML
        // parsing and validating.

        final String dataContextId;

        try {
            final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
            final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
            final InputStream in = new ByteArrayInputStream(xmlData.getBytes(XmlUtility.CHARACTER_ENCODING));
            final Document result = docBuilder.parse(new InputSource(in));
            result.getDocumentElement().normalize();
            final Node n = XPathAPI.selectSingleNode(result, "//properties/context/@href");
            final String contextHref = n.getNodeValue();
            dataContextId = contextHref.substring(contextHref.lastIndexOf('/') + 1);
        } catch (final ParserConfigurationException e) {
            throw new SystemException(e);
        } catch (final SAXException e) {
            throw new SystemException(e);
        } catch (final IOException e) {
            throw new SystemException(e);
        } catch (final TransformerException e) {
            throw new SystemException(e);
        }

        if (!dataContextId.equals(tripleStoreUtility.getContext(id))) {
            throw new InvalidContextException("Objects are not in same Context.");
        }

    }

    public String getPath(final String id, final String newVersionNumber)
            throws WebserverSystemException, TripleStoreSystemException {

        final StringBuilder result = new StringBuilder("/");

        final String objectType = tripleStoreUtility.getObjectType(id);
        if (Constants.ITEM_OBJECT_TYPE.equals(objectType) || Constants.CONTAINER_OBJECT_TYPE.equals(objectType)
                || Constants.CONTEXT_OBJECT_TYPE.equals(objectType)) {
            result.append("ir/");
        } else {
            throw new WebserverSystemException("Can not create path for object-type " + objectType + '.');
        }

        result.append(objectType);
        result.append('/');
        result.append(id);
        if (newVersionNumber != null) {
            result.append(':');
            result.append(newVersionNumber);
        }

        return result.toString();
    }

    /**
     * Create new object version.
     *
     * @param versionComment Comment of event (version or status change)
     * @param newStatus      New status of object.
     * @param resource       resource object.
     * @throws SystemException Thrown in case of an internal system error.
     */
    public void makeVersion(final String versionComment, final String newStatus, final VersionableResource resource)
            throws SystemException {

        final String comment = createComment(resource, newStatus, versionComment);

        final Map<String, String> resBaseData = getResourceBaseData(resource);
        final Map<String, StartElementWithChildElements> updateElementsRelsExt = new TreeMap<String, StartElementWithChildElements>();
        final Map<String, List<StartElementWithChildElements>> removeElementsRelsExt = new TreeMap<String, List<StartElementWithChildElements>>();
        final Map<String, String> currentVersionProperties = resource.getResourceProperties();

        // elements to update in RELS-EXT in any case
        // - latest-version.date
        // - latest-version.user
        final StartElementWithChildElements modifiedBy = new StartElementWithChildElements(
                Elements.ELEMENT_MODIFIED_BY, Constants.STRUCTURAL_RELATIONS_NS_URI,
                Constants.STRUCTURAL_RELATIONS_NS_PREFIX, null, getCurrentUserId(), null);
        final Attribute resourceAttribute = new Attribute("resource", Constants.RDF_NAMESPACE_URI,
                Constants.RDF_NAMESPACE_PREFIX, Constants.IDENTIFIER_PREFIX + getCurrentUserId());
        modifiedBy.addAttribute(resourceAttribute);
        updateElementsRelsExt.put(Elements.ELEMENT_MODIFIED_BY, modifiedBy);

        updateElementsRelsExt.put(Elements.ELEMENT_MODIFIED_BY_TITLE,
                new StartElementWithChildElements(Elements.ELEMENT_MODIFIED_BY_TITLE, Constants.PROPERTIES_NS_URI,
                        Constants.PROPERTIES_NS_PREFIX, null, getCurrentUserRealName(), null));

        final String buildNumber = getBuildNumber();
        updateElementsRelsExt.put(XmlTemplateProviderConstants.BUILD_NUMBER,
                new StartElementWithChildElements(XmlTemplateProviderConstants.BUILD_NUMBER,
                        "http://escidoc.de/core/01/system/", "system", null, buildNumber, null));

        boolean release = false;
        if (newStatus != null) {

            release = false;
            // set new event entry and update some values in RELS-EXT,
            // version
            // renew in RELS-EXT:
            // - status
            // - latest-version.date ( done )
            // - latest-version.user ( done )
            // - latest-version.comment ( done )
            // - latest-version.status

            // if status is already "released", leave it untouched
            // unless withdraw
            if (!Constants.STATUS_RELEASED.equals(currentVersionProperties.get(PropertyMapKeys.PUBLIC_STATUS))
                    || Constants.STATUS_WITHDRAWN.equals(newStatus)) {
                updateElementsRelsExt.put(Elements.ELEMENT_PUBLIC_STATUS,
                        new StartElementWithChildElements(Elements.ELEMENT_PUBLIC_STATUS,
                                Constants.PROPERTIES_NS_URI, Constants.PROPERTIES_NS_PREFIX, null, newStatus,
                                null));
                // - public-status-comment
                updateElementsRelsExt.put(Elements.ELEMENT_PUBLIC_STATUS_COMMENT,
                        new StartElementWithChildElements(Elements.ELEMENT_PUBLIC_STATUS_COMMENT,
                                Constants.PROPERTIES_NS_URI, Constants.PROPERTIES_NS_PREFIX, null, comment, null));
            }
            if (!Constants.STATUS_WITHDRAWN.equals(newStatus)) {
                updateElementsRelsExt.put(Elements.ELEMENT_STATUS,
                        new StartElementWithChildElements(Elements.ELEMENT_STATUS, Constants.VERSION_NS_URI,
                                Constants.VERSION_NS_PREFIX, null, newStatus, null));
                // - latest-version.comment
                updateElementsRelsExt.put(Elements.ELEMENT_COMMENT,
                        new StartElementWithChildElements(Elements.ELEMENT_COMMENT, Constants.VERSION_NS_URI,
                                Constants.VERSION_NS_PREFIX, null, comment, null));
            }

            if (Constants.STATUS_RELEASED.equals(newStatus)) {
                release = true;
                resource.setVersionStatusChange();
                resource.setVersionStatus(Constants.STATUS_RELEASED);

                // renew in RELS-EXT:
                // - public-status-comment
                // - latest-release.number
                // - latest-release.date
                // - latest-release.pid
                updateElementsRelsExt.put(Elements.ELEMENT_PUBLIC_STATUS_COMMENT,
                        new StartElementWithChildElements(Elements.ELEMENT_PUBLIC_STATUS_COMMENT,
                                Constants.PROPERTIES_NS_URI, Constants.PROPERTIES_NS_PREFIX, null, comment, null));
                updateElementsRelsExt.put(Elements.ELEMENT_NUMBER,
                        new StartElementWithChildElements(Elements.ELEMENT_NUMBER, Constants.RELEASE_NS_URI,
                                Constants.RELEASE_NS_PREFIX, null,
                                currentVersionProperties.get(PropertyMapKeys.LATEST_VERSION_NUMBER), null));

                // SWA: The release date has to be the same as the version/date
                // which is written as latest.

                // update latest-release/pid (properties/release/pid)
                updateElementsRelsExt.put(Constants.RELEASE_NS_URI + Elements.ELEMENT_PID,
                        new StartElementWithChildElements(Elements.ELEMENT_PID, Constants.RELEASE_NS_URI,
                                Constants.RELEASE_NS_PREFIX, null,
                                currentVersionProperties.get(PropertyMapKeys.CURRENT_VERSION_PID), null));
            }

            // now, everything except version-history is written. So
            // update the timestamp to use it in version-history
            // and write it to RELS-EXT. But the timestamp is obtained from
            // first RELS-EXT wrote, which is done in the persist method. That's
            // why here only the XML structure with a placeholder is written.
            // The placeholder is later (during the persist method) replaced
            // with the exact timestamp.

            // update version-history
            final Map<String, StartElementWithChildElements> updateElementsWOV = new HashMap<String, StartElementWithChildElements>();

            if (!Constants.STATUS_WITHDRAWN.equals(newStatus)) {
                // change first occurrence of comment in version-history
                updateElementsWOV.put(TripleStoreUtility.PROP_VERSION_COMMENT,
                        new StartElementWithChildElements(TripleStoreUtility.PROP_VERSION_COMMENT,
                                Constants.WOV_NAMESPACE_URI, Constants.WOV_NAMESPACE_PREFIX, null, comment, null));
                // change first occurrence of version-status in version-history
                // but not for withdraw
                updateElementsWOV.put(TripleStoreUtility.PROP_VERSION_STATUS,
                        new StartElementWithChildElements(TripleStoreUtility.PROP_VERSION_STATUS,
                                Constants.WOV_NAMESPACE_URI, Constants.WOV_NAMESPACE_PREFIX, null, newStatus,
                                null));
            }
            final List<StartElementWithChildElements> elementsToAdd = new ArrayList<StartElementWithChildElements>();
            // elementsToAdd.add(versionStatus);

            // add premis:event to version-history/version[1]/events as
            // first child
            final String newEventEntry = createEventXml(resource.getId(), resBaseData.get(RESOURCE_BASE_URL),
                    getCurrentUserRealName(), getCurrentUserId(),
                    XmlTemplateProviderConstants.TIMESTAMP_PLACEHOLDER, newStatus, comment,
                    currentVersionProperties);

            // change /version-history/version[version-number='x']/timestamp
            // this method changes exactly the first occurrence of the timestamp
            // - which is in this case the same (A support for XPath expressions
            // is not given due this parsers.)
            updateElementsWOV.put(Constants.WOV_NAMESPACE_URI + "timestamp",
                    new StartElementWithChildElements("timestamp", Constants.WOV_NAMESPACE_URI,
                            Constants.WOV_NAMESPACE_PREFIX, null,
                            XmlTemplateProviderConstants.TIMESTAMP_PLACEHOLDER, null, 1));

            // do update WOV
            writeEvent(resource, newEventEntry, updateElementsWOV, elementsToAdd);

            // Update the status in the resource
            if (!Constants.STATUS_WITHDRAWN.equals(newStatus)) {
                resource.setVersionStatus(newStatus);
            }
        } else { // if (newStatus == null)
                 // this is an update

            // - latest-version.comment
            updateElementsRelsExt.put(Elements.ELEMENT_COMMENT,
                    new StartElementWithChildElements(Elements.ELEMENT_COMMENT, Constants.VERSION_NS_URI,
                            Constants.VERSION_NS_PREFIX, null, comment, null));

            // create new version entry
            // renew in RELS-EXT:
            // - latest-version.number
            // - latest-version.date ( done )
            // - latest-version.status (unchanged)
            // - latest-version.valid-status (unchanged)
            // - latest-version.user ( done )
            // - latest-version.comment ( done )

            final int newVersionNumberInt = Integer
                    .parseInt(currentVersionProperties.get(PropertyMapKeys.LATEST_VERSION_NUMBER)) + 1;

            updateElementsRelsExt.put(Elements.ELEMENT_NUMBER,
                    new StartElementWithChildElements(Elements.ELEMENT_NUMBER, Constants.VERSION_NS_URI,
                            Constants.VERSION_NS_PREFIX, null, Integer.toString(newVersionNumberInt), null));

            if (Constants.STATUS_RELEASED
                    .equals(currentVersionProperties.get(PropertyMapKeys.LATEST_VERSION_VERSION_STATUS))) {
                // update of resource in state released
                // change latest-version.status to pending

                updateElementsRelsExt.put(Elements.ELEMENT_STATUS,
                        new StartElementWithChildElements(Elements.ELEMENT_STATUS, Constants.VERSION_NS_URI,
                                Constants.VERSION_NS_PREFIX, null, Constants.STATUS_PENDING, null));

                currentVersionProperties.put(PropertyMapKeys.LATEST_VERSION_VERSION_STATUS,
                        Constants.STATUS_PENDING);
            }

            // remove version.pid
            final StartElementWithChildElements propertyToDelete = new StartElementWithChildElements(
                    Constants.RELEASE_NS_URI + Elements.ELEMENT_PID, Constants.VERSION_NS_URI,
                    Constants.VERSION_NS_PREFIX, null, null, null);
            final List<StartElementWithChildElements> toRemove = new ArrayList<StartElementWithChildElements>();
            toRemove.add(propertyToDelete);
            removeElementsRelsExt.put("/RDF/Description/pid", toRemove);

            // write version-history (Keep in mind, that at this point a
            // placeholder for the timestamp is written. This placeholder is
            // replaced during persist().)
            final String newVersionXml = createVersionXml(resource, resBaseData, currentVersionProperties,
                    newVersionNumberInt, comment);

            prependVersion(resource, newVersionXml);
        }

        // last operation is to update the timestamp in RELS-EXT
        if (release) {
            resource.setLatestReleaseVersionNumber(
                    currentVersionProperties.get(PropertyMapKeys.CURRENT_VERSION_VERSION_NUMBER));
        }
        // write changes to RELS-EXT
        updateElementsInRelsExt(updateElementsRelsExt, removeElementsRelsExt, resource,
                currentVersionProperties.get(PropertyMapKeys.PUBLIC_STATUS), release);
    }

    /**
     * Update elements in RELS-EXt of versionable resource.
     *
     * @param updateElementsRelsExt elements to update
     * @param removeElementsRelsExt elements to remove from RELS-EXT
     * @param resource              object resource
     * @param currentPublicStatus   public-status of current version
     * @param release               set true if version is release, false otherwise
     * @throws SystemException Thrown in case of internal failure.
     */
    private static void updateElementsInRelsExt(
            final Map<String, StartElementWithChildElements> updateElementsRelsExt,
            final Map<String, List<StartElementWithChildElements>> removeElementsRelsExt,
            final FedoraResource resource, final String currentPublicStatus, final boolean release)
            throws SystemException {

        final StaxParser sp = new StaxParser();
        final ItemRelsExtUpdateHandler itemRelsExtUpdateHandler = new ItemRelsExtUpdateHandler(
                updateElementsRelsExt, sp);
        sp.addHandler(itemRelsExtUpdateHandler);

        final HashMap<String, String> pathes = new HashMap<String, String>();
        pathes.put("/RDF", null);
        final MultipleExtractor me = new MultipleExtractor(pathes, sp);
        me.removeElements(removeElementsRelsExt);
        sp.addHandler(me);

        try {
            final ByteArrayInputStream relsExtBA;
            if (release && !Constants.STATUS_RELEASED.equals(currentPublicStatus)) {

                // FIXME if FIRST release add "latest-release" properties but
                // not by string replace
                String relsExtS = new String(resource.getRelsExt().getStream(), XmlUtility.CHARACTER_ENCODING);
                relsExtS = relsExtS.replaceFirst("(</rdf:Description>)",
                        '<' + Constants.RELEASE_NS_PREFIX + ':' + Elements.ELEMENT_NUMBER + " xmlns:"
                                + Constants.RELEASE_NS_PREFIX + "=\"" + Constants.RELEASE_NS_URI + "\"/>\n<"
                                + Constants.RELEASE_NS_PREFIX + ':' + Elements.ELEMENT_DATE + " xmlns:"
                                + Constants.RELEASE_NS_PREFIX + "=\"" + Constants.RELEASE_NS_URI + "\">---</"
                                + Constants.RELEASE_NS_PREFIX + ':' + Elements.ELEMENT_DATE + ">\n" + "$1");
                relsExtBA = new ByteArrayInputStream(relsExtS.getBytes(XmlUtility.CHARACTER_ENCODING));
            } else {
                relsExtBA = new ByteArrayInputStream(resource.getRelsExt().getStream());
            }
            sp.parse(relsExtBA);
            final ByteArrayOutputStream relsExt = (ByteArrayOutputStream) me.getOutputStreams().get("RDF");
            resource.setRelsExt(relsExt.toString(XmlUtility.CHARACTER_ENCODING));
        } catch (final NullPointerException e) { // TODO: Refactor this! Don't use exceptions for controll flow!
            throw new XmlParserSystemException(e);
        } catch (final XMLStreamException e) {
            throw new XmlParserSystemException(e);
        } catch (final UnsupportedEncodingException e) {
            throw new EncodingSystemException(e);
        } catch (final LockingException e) {
            throw new IntegritySystemException(e);
        } catch (final Exception e) {
            throw new SystemException("Unexpected Exception.", e);
        }

    }

    /**
     * Create a new entry for the version-history (WOV).
     *
     * @param resource The versionable resource.
     * @param resBaseData
     * @param currentVersionProperties
     * @param newVersionNumberInt
     * @param comment  The version comment.
     * @return WOV entry for new version
     * @throws de.escidoc.core.common.exceptions.system.WebserverSystemException
     */
    private String createVersionXml(final FedoraResource resource, final Map<String, String> resBaseData,
            final Map<String, String> currentVersionProperties, final int newVersionNumberInt, final String comment)
            throws WebserverSystemException {

        // Map for version entry values
        final Map<String, String> newVersionEntry = new HashMap<String, String>();

        // compute new latest version data
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_NAMESPACE_PREFIX, Constants.WOV_NAMESPACE_PREFIX);
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_NAMESPACE, Constants.WOV_NAMESPACE_URI);
        newVersionEntry.put(XmlTemplateProviderConstants.OBJID,
                resource.getId() + ':' + Integer.toString(newVersionNumberInt));
        newVersionEntry.put(XmlTemplateProviderConstants.TITLE, "Version " + Integer.toString(newVersionNumberInt));

        newVersionEntry.put(XmlTemplateProviderConstants.HREF,
                resource.getHref() + ':' + Integer.toString(newVersionNumberInt));
        newVersionEntry.put(XmlTemplateProviderConstants.VERSION_NUMBER, Integer.toString(newVersionNumberInt));
        // real timestamp is not clear at this point (will pre fixed lated
        // during persist)
        // newVersionEntry.put(XmlTemplateProviderConstants.TIMESTAMP,
        // newLatestModificationTimestamp);
        newVersionEntry.put(XmlTemplateProviderConstants.TIMESTAMP,
                XmlTemplateProviderConstants.TIMESTAMP_PLACEHOLDER);
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_STATUS,
                currentVersionProperties.get(PropertyMapKeys.LATEST_VERSION_VERSION_STATUS));
        newVersionEntry.put(XmlTemplateProviderConstants.VALID_STATUS,
                currentVersionProperties.get(PropertyMapKeys.LATEST_VERSION_VALID_STATUS));
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_COMMENT,
                XmlUtility.escapeForbiddenXmlCharacters(comment));
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_AGENT_ID_VALUE, getCurrentUserId());
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_AGENT_ID_TYPE, Constants.PREMIS_ID_TYPE_ESCIDOC);
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_AGENT_BASE_URI, Constants.USER_ACCOUNT_URL_BASE);
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_AGENT_TITLE, getCurrentUserRealName());

        newVersionEntry.put(XmlTemplateProviderConstants.VAR_EVENT_TYPE, "update");
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_EVENT_XMLID,
                'v' + Integer.toString(newVersionNumberInt) + 'e' + System.currentTimeMillis());
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_EVENT_ID_VALUE,
                resBaseData.get(RESOURCE_BASE_URL) + resource.getId() + "/resources/"
                        + Elements.ELEMENT_WOV_VERSION_HISTORY + '#'
                        + newVersionEntry.get(XmlTemplateProviderConstants.VAR_EVENT_XMLID));
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_EVENT_ID_TYPE, Constants.PREMIS_ID_TYPE_URL_RELATIVE);
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_OBJECT_ID_TYPE, Constants.PREMIS_ID_TYPE_ESCIDOC);
        newVersionEntry.put(XmlTemplateProviderConstants.VAR_OBJECT_ID_VALUE, resource.getId());
        // get xml representation of new version
        return CommonFoXmlProvider.getInstance().getWovVersionEntryXml(newVersionEntry);
    }

    /**
     * Set event values to HashMap and call version-history event entry.
     *
     * @param resourceId               The id of the resource.
     * @param resourceBaseUrl          The resource base URL
     * @param currentUserName          The name of the current user.
     * @param currentUserId            The id of the current user.
     * @param latestModificationTimestamp
     * @param newStatus                New version-status
     * @param comment                  The version comment.
     * @param currentVersionProperties map with properties of current version
     * @return XML event entry for version-history
     * @throws WebserverSystemException Thrown in case of internal error.
     */
    private static String createEventXml(final String resourceId, final String resourceBaseUrl,
            final String currentUserName, final String currentUserId, final String latestModificationTimestamp,
            final String newStatus, final String comment, final Map<String, String> currentVersionProperties)
            throws WebserverSystemException {

        final HashMap<String, String> eventValues = new HashMap<String, String>();
        eventValues.put(XmlTemplateProviderConstants.VAR_EVENT_TYPE, newStatus);
        eventValues.put(XmlTemplateProviderConstants.VAR_EVENT_XMLID,
                'v' + currentVersionProperties.get(PropertyMapKeys.LATEST_VERSION_NUMBER) + 'e'
                        + System.currentTimeMillis());
        eventValues.put(XmlTemplateProviderConstants.VAR_EVENT_ID_TYPE, Constants.PREMIS_ID_TYPE_URL_RELATIVE);
        eventValues.put(XmlTemplateProviderConstants.VAR_EVENT_ID_VALUE,
                resourceBaseUrl + resourceId + "/resources/" + Elements.ELEMENT_WOV_VERSION_HISTORY + '#'
                        + eventValues.get(XmlTemplateProviderConstants.VAR_EVENT_XMLID));
        eventValues.put(XmlTemplateProviderConstants.TIMESTAMP, latestModificationTimestamp);
        eventValues.put(XmlTemplateProviderConstants.VAR_COMMENT, XmlUtility.escapeForbiddenXmlCharacters(comment));
        eventValues.put(XmlTemplateProviderConstants.VAR_AGENT_BASE_URI, Constants.USER_ACCOUNT_URL_BASE);
        eventValues.put(XmlTemplateProviderConstants.VAR_AGENT_TITLE, currentUserName);
        eventValues.put(XmlTemplateProviderConstants.VAR_AGENT_ID_TYPE, Constants.PREMIS_ID_TYPE_ESCIDOC);
        eventValues.put(XmlTemplateProviderConstants.VAR_AGENT_ID_VALUE, currentUserId);
        eventValues.put(XmlTemplateProviderConstants.VAR_OBJECT_ID_TYPE, Constants.PREMIS_ID_TYPE_ESCIDOC);
        eventValues.put(XmlTemplateProviderConstants.VAR_OBJECT_ID_VALUE, resourceId);

        return CommonFoXmlProvider.getInstance().getPremisEventXml(eventValues);
    }

    /**
     * Create comment for version update.
     *
     * @param resource       The versionable resource.
     * @param newStatus      New status of resource.
     * @param versionComment Comment for version.
     * @return Comment
     */
    private static String createComment(final FedoraResource resource, final String newStatus,
            final String versionComment) {
        String comment = versionComment;
        if (versionComment == null) {
            comment = newStatus != null ? "Status changed to " + newStatus : "New version created";
            comment += " for " + resource.getClass().getSimpleName() + ' ' + resource.getId() + '.';
        }
        return comment;
    }

    /**
     * @return id of current user.
     * @throws WebserverSystemException Thrown if current user is not set.
     */
    public String getCurrentUserId() throws WebserverSystemException {
        return getCurrentUser()[0];
    }

    /**
     * @return real name of current user.
     * @throws WebserverSystemException Thrown if current user is not set.
     */
    public String getCurrentUserRealName() throws WebserverSystemException {
        return getCurrentUser()[1];
    }

    /**
     * Get basic XML data from resource.
     *
     * @param resource the resource.
     * @return Map with baseURL, xml namespace and prefix
     * @throws SystemException Thrown if anything fails.
     */
    private static Map<String, String> getResourceBaseData(final VersionableResource resource)
            throws SystemException {

        final Map<String, String> baseData = new HashMap<String, String>();
        if (resource instanceof Item) {
            baseData.put(RESOURCE_BASE_URL, Constants.ITEM_URL_BASE);
            baseData.put("resourcePrefix", Constants.ITEM_PROPERTIES_NAMESPACE_PREFIX);
            baseData.put("resourceNamespace", Constants.ITEM_PROPERTIES_NAMESPACE_URI);
        } else if (resource instanceof Container) {
            baseData.put(RESOURCE_BASE_URL, Constants.CONTAINER_URL_BASE);
            baseData.put("resourcePrefix", Constants.CONTAINER_PROPERTIES_PREFIX);
            baseData.put("resourceNamespace", Constants.CONTAINER_PROPERTIES_NAMESPACE_URI);
        } else if (resource instanceof ContentModel) {
            baseData.put(RESOURCE_BASE_URL, Constants.CONTENT_MODEL_URL_BASE);
            baseData.put("resourcePrefix", Constants.CONTENT_MODEL_NAMESPACE_PREFIX);
            baseData.put("resourceNamespace", Constants.CONTENT_MODEL_NAMESPACE_URI);
        } else if (resource instanceof Relation) {
            // FIXME resourceBaseUrl = Constants.CONTENT_RELATIONS_URL_BASE;
            baseData.put("resourcePrefix", Constants.CONTENT_RELATIONS_NAMESPACE_PREFIX);
            baseData.put("resourceNamespace", Constants.CONTENT_RELATIONS_NAMESPACE_URI);
        } else {
            throw new SystemException(
                    "'makeVersion' not supported for object-type '" + resource.getClass().getName() + "'.");
        }

        return baseData;
    }

    private static void prependVersion(final VersionableResource resource, final String versionEntry)
            throws EncodingSystemException, IntegritySystemException {

        // TODO insert new version in version-history
        try {
            final String wov = resource.getWov().toString(XmlUtility.CHARACTER_ENCODING);
            final String newWov = wov.replaceFirst(
                    "(<" + Constants.WOV_NAMESPACE_PREFIX + ":version-history[^>]+>)", "$1" + versionEntry);
            final Datastream ds = new Datastream("version-history", resource.getId(),
                    newWov.getBytes(XmlUtility.CHARACTER_ENCODING), MediaType.TEXT_XML.toString());
            resource.setWov(ds);
        } catch (final StreamNotFoundException e) {
            throw new IntegritySystemException(e);
        } catch (final UnsupportedEncodingException e) {
            throw new EncodingSystemException(e);
        } catch (final LockingException e) {
            // we just updated the item!
            throw new IntegritySystemException(e);
        } catch (final SystemException e) {
            // FIXME remove SystemException from resource.setWov(ds)
            throw new IntegritySystemException(e);
        }
    }

    private static void writeEvent(final VersionableResource resource, final String newEventEntry,
            final Map<String, StartElementWithChildElements> updateElementsWOV,
            final List<StartElementWithChildElements> elementsToAdd) throws WebserverSystemException {

        final StaxParser sp = new StaxParser();
        final ItemRelsExtUpdateHandler ireuh = new ItemRelsExtUpdateHandler(updateElementsWOV, sp);
        ireuh.setPath("/version-history/version/");
        sp.addHandler(ireuh);
        final AddNewSubTreesToDatastream addNewSubtreesHandler = new AddNewSubTreesToDatastream("/version-history",
                sp);
        final StartElement pointer = new StartElement("version", Constants.WOV_NAMESPACE_URI, "escidocVersions",
                null);
        addNewSubtreesHandler.setPointerElement(pointer);
        addNewSubtreesHandler.setSubtreeToInsert(elementsToAdd);
        sp.addHandler(addNewSubtreesHandler);

        try {
            sp.parse(resource.getWov().getStream());
            sp.clearHandlerChain();
            final ByteArrayOutputStream newWovStream = addNewSubtreesHandler.getOutputStreams();
            final String newWovString = newWovStream.toString(XmlUtility.CHARACTER_ENCODING)
                    .replaceFirst("(<" + Constants.WOV_NAMESPACE_PREFIX + ":events[^>]*>)", "$1" + newEventEntry);

            resource.setWov(new Datastream(Elements.ELEMENT_WOV_VERSION_HISTORY, resource.getId(),
                    newWovString.getBytes(XmlUtility.CHARACTER_ENCODING), MediaType.TEXT_XML.toString()));
        } catch (final Exception e) {
            throw new WebserverSystemException(e);
        }

    }

    /**
     * Adds and removes the provided elements to/from RELS-EXT.
     *
     * @param addToRelsExt      - Vector with elements, which should be added to RELS-EXT
     * @param deleteFromRelsExt - Map containing key-value pairs: keys - paths to elements, which should be deleted from
     *                          RELS-EXT and values - elements thereself.
     * @param relsExtBytes      - optional parameter: byte [] with content of RELS-EXT datastream. If no relsExtBytes
     *                          provided (relsExtBytes is set to null), the method retrieves the RELS-EXT from Fedora
     * @param resource          The resource which RELS-EXT is to alter.
     * @param updateProperties
     * @return byte [] RELS-EXT datastream content
     * @throws IntegritySystemException If the integrity of the repository is violated.
     * @throws XmlParserSystemException If parsing of xml data fails.
     * @throws WebserverSystemException In case of an internal error.
     * @throws FedoraSystemException    If the Fedora reports an error
     */
    public static byte[] updateRelsExt(final List<StartElementWithChildElements> addToRelsExt,
            final Map<String, List<StartElementWithChildElements>> deleteFromRelsExt, final byte[] relsExtBytes,
            final FedoraResource resource, final Map<String, StartElementWithChildElements> updateProperties)
            throws IntegritySystemException, FedoraSystemException, XmlParserSystemException,
            WebserverSystemException {

        final byte[] relsExtContent;
        if (relsExtBytes == null) {
            try {
                relsExtContent = resource.getRelsExt().getStream();
            } catch (final StreamNotFoundException e1) {
                throw new IntegritySystemException("Stream not found.", e1);
            }
        } else {
            relsExtContent = relsExtBytes;
        }
        ByteArrayInputStream relsExtIs = new ByteArrayInputStream(relsExtContent);

        final StaxParser sp = new StaxParser();
        byte[] relsExtNewBytes = null;
        boolean updatedRelsExtProperties = false;
        if (addToRelsExt != null && !addToRelsExt.isEmpty()) {
            if (updateProperties != null && !updateProperties.isEmpty()) {
                updatedRelsExtProperties = true;
                final ItemRelsExtUpdateHandler itemRelsExtUpdateHandler = new ItemRelsExtUpdateHandler(
                        updateProperties, sp);
                sp.addHandler(itemRelsExtUpdateHandler);

            }
            final AddNewSubTreesToDatastream addNewEntriesHandler = new AddNewSubTreesToDatastream("/RDF", sp);

            final StartElement pointer = new StartElement();
            pointer.setLocalName("Description");
            pointer.setPrefix(Constants.RDF_NAMESPACE_PREFIX);
            pointer.setNamespace(Constants.RDF_NAMESPACE_URI);

            addNewEntriesHandler.setPointerElement(pointer);

            addNewEntriesHandler.setSubtreeToInsert(addToRelsExt);

            sp.addHandler(addNewEntriesHandler);

            try {
                sp.parse(relsExtIs);
            } catch (final XMLStreamException e) {
                throw new XmlParserSystemException(e.getMessage(), e);
            } catch (final NullPointerException e) {
                throw new XmlParserSystemException(e);
            } catch (final Exception e) {
                throw new WebserverSystemException(e);
            }
            sp.clearHandlerChain();
            final ByteArrayOutputStream relsExtNewStream = addNewEntriesHandler.getOutputStreams();
            relsExtNewBytes = relsExtNewStream.toByteArray();
        }

        if (deleteFromRelsExt != null && !deleteFromRelsExt.isEmpty()) {

            if (relsExtNewBytes != null) {
                relsExtIs = new ByteArrayInputStream(relsExtNewBytes);
            }
            if (updateProperties != null && !updateProperties.isEmpty() && !updatedRelsExtProperties) {
                updatedRelsExtProperties = true;
                final ItemRelsExtUpdateHandler itemRelsExtUpdateHandler = new ItemRelsExtUpdateHandler(
                        updateProperties, sp);
                sp.addHandler(itemRelsExtUpdateHandler);

            }
            final HashMap<String, String> extractPathes = new HashMap<String, String>();

            extractPathes.put("/RDF", null);

            final MultipleExtractor multipleExtractor = new MultipleExtractor(extractPathes, sp);

            multipleExtractor.removeElements(deleteFromRelsExt);
            sp.addHandler(multipleExtractor);

            try {
                sp.parse(relsExtIs);
            } catch (final XMLStreamException e) {
                throw new XmlParserSystemException(e.getMessage(), e);
            } catch (final NullPointerException e) {
                throw new XmlParserSystemException(e);
            } catch (final Exception e) {
                throw new WebserverSystemException(e);
            }
            sp.clearHandlerChain();
            final Map<String, Object> streams = multipleExtractor.getOutputStreams();
            final ByteArrayOutputStream relsExtNewStream = (ByteArrayOutputStream) streams.get("RDF");
            relsExtNewBytes = relsExtNewStream.toByteArray();
        }
        if (updateProperties != null && !updateProperties.isEmpty() && !updatedRelsExtProperties) {
            final ItemRelsExtUpdateHandler itemRelsExtUpdateHandler = new ItemRelsExtUpdateHandler(updateProperties,
                    sp);
            sp.addHandler(itemRelsExtUpdateHandler);
            final HashMap<String, String> pathes = new HashMap<String, String>();
            pathes.put("/RDF", null);
            final MultipleExtractor multipleExtractor = new MultipleExtractor(pathes, sp);
            sp.addHandler(multipleExtractor);
            try {
                sp.parse(relsExtIs);
            } catch (final NullPointerException e) {
                throw new XmlParserSystemException(e);
            } catch (final XMLStreamException e) {
                throw new XmlParserSystemException(e);
            } catch (final LockingException e) {
                throw new IntegritySystemException(e);
            } catch (final Exception e) {
                throw new WebserverSystemException("Unexpected Exception.", e);
            }
            sp.clearHandlerChain();
            final Map<String, Object> streams = multipleExtractor.getOutputStreams();
            final ByteArrayOutputStream relsExtNewStream = (ByteArrayOutputStream) streams.get("RDF");
            relsExtNewBytes = relsExtNewStream.toByteArray();
        }
        return relsExtNewBytes != null ? relsExtNewBytes : relsExtContent;

    }

    /**
     * The method uses the staging file handler to create a new staging file that stores the provided stream in to
     * staging area. The URL to this staging file is extracted from the XML representation of the staging file and is
     * returned as the redirect URL.
     *
     * @param streamContent stream content
     * @param fileName      file name, which will be included in returned redirect url
     * @param mimeType      mime type
     * @return redirectUrl url
     * @throws FileSystemException In case of an internal error during storing the content.
     */
    public String upload(final byte[] streamContent, final String fileName, final String mimeType)
            throws FileSystemException {

        final EscidocBinaryContent content = new EscidocBinaryContent();
        try {
            content.setFileName(fileName);
            content.setMimeType(mimeType);
            content.setContent(new ByteArrayInputStream(streamContent));
        } catch (IOException e) {
            throw new FileSystemException(e.getMessage(), e);
        }
        final String stagingFileXml;

        try {
            stagingFileXml = stagingFileHandler.create(content);
        } catch (final SystemException e) {
            throw new FileSystemException(e.getMessage(), e);
        } catch (final Exception e) {
            throw new FileSystemException("Unexpected exception from StagingFileHandler.create", e);
        }
        final Matcher matcher = REDIRECT_URL_PATTERN.matcher(stagingFileXml);
        if (matcher.find()) {
            final String base = matcher.group(1);
            final String path = matcher.group(2);
            if (base != null && path != null) {
                return base + path;
            }
        }
        throw new FileSystemException("Unparseable result from StagingFileHandler.create");
    }

    /**
     * Prepare the XML for the return of all task oriented methods.
     * <p/>
     * TODO either use velocity template and/or move to an own class (ReturnXY)
     *
     * @param lastModificationDate The last-modification-date of the resource.
     * @return The result XML structure.
     * @throws SystemException Thrown if parsing last modification or retrieving xml:base failed.
     */
    public String prepareReturnXmlFromLastModificationDate(final DateTime lastModificationDate) {
        return prepareReturnXml(lastModificationDate, null);
    }

    public String prepareReturnXml(final String content) {
        return prepareReturnXml(null, content);
    }

    /**
     * Prepare the XML for the return of all task oriented methods.
     * <p/>
     * TODO either use velocity template and/or move to an own class (ReturnXY)
     *
     * @param lastModificationDate The last-modification-date of the resource.
     * @param content              The XML content elements of the result structure.
     * @return The result XML structure.
     * @throws SystemException Thrown if parsing last modification or retrieving xml:base failed.
     */
    public static String prepareReturnXml(DateTime lastModificationDate, final String content) {

        if (lastModificationDate == null) {
            lastModificationDate = new DateTime(DateTimeZone.UTC);
        }

        String xml = Constants.XML_HEADER + "<result " + "xmlns=\"" + Constants.RESULT_NAMESPACE_URI + "\" "
                + "last-modification-date=\"" + lastModificationDate.toString() + '\"';

        xml += content == null ? " />" : ">\n" + content + "</result>\n";

        return xml;
    }

    /**
     * Makes a given URL full qualified by prepending eSciDoc base URL if necessary. Returns null, if the IDs of an item
     * and a component are given and the URL refers to the content of that component of that item. Throws an Exception
     * if the URL is invalid in the given context; that means it is of a form that is not allowed.
     *
     * @param url         A URL.
     * @param itemId      The id of an Item or null.
     * @param componentId The id of a Component or null.
     * @return The full qualified URL or null if the URL refers to the content of the component, identified by
     *         componentId, of the item, identified by itemId.
     * @throws InvalidContentException  If the String given as url is not a URI or invalid in the current context.
     * @throws WebserverSystemException If an error occurs.
     */
    public static String processUrl(final String url, final String itemId, final String componentId)
            throws InvalidContentException {
        final String escidocBaseUrl = XmlUtility.getEscidocBaseUrl();

        try {
            // // FIXME workaround issue 631
            // if (url.startsWith(fedoraBaseUrl)
            // || url.startsWith(fedoraBaseUrl.replaceFirst("localhost",
            // "127.0.0.1"))) {
            // throw new InvalidContentException(
            // "Direct access to Fedora denied: '" + url + "'.");
            // }
            // given
            final URI local;
            final URI fq;
            if ((int) url.charAt(0) == (int) '/') {
                checkESciDocLocalURL(url);
                local = new URI(url);
                fq = new URI(escidocBaseUrl + url);
            } else {
                local = new URI(url.replaceFirst("http[s]?://[^/]+", ""));
                fq = new URI(url);
            }

            if (itemId != null && componentId != null) {
                // expected
                final String thisUrl = Constants.ITEM_URL_BASE + itemId
                        + de.escidoc.core.common.business.fedora.Constants.COMPONENT_URL_PART + componentId
                        + de.escidoc.core.common.business.fedora.Constants.COMPONENT_CONTENT_URL_PART;
                final URI thisLocal = new URI(thisUrl);
                final URI thisFq = new URI(escidocBaseUrl + thisUrl);

                // recognize the URL we send
                if (local.equals(thisLocal) || fq.equals(thisFq)) {
                    return null;
                }
            }
            return fq.toString();
        } catch (final URISyntaxException e) {
            throw new InvalidContentException("No valid URL.", e);
        }

    }

    /**
     * Get number of build from escidoc.properties.
     *
     * @return build number
     * @throws WebserverSystemException Thrown if obtaining from properties failed.
     */
    public static String getBuildNumber() throws WebserverSystemException {
        final String buildNumber;
        try {
            buildNumber = EscidocConfiguration.getInstance().get(EscidocConfiguration.BUILD_NUMBER);
        } catch (final Exception e) {
            throw new WebserverSystemException(
                    "Failed to retrieve configuration parameter " + EscidocConfiguration.FEDORA_URL, e);
        }
        return buildNumber;
    }

    /**
     * Check if an URL is an URL to the framework itself.
     *
     * @param url URL as String.
     * @throws InvalidContentException Thrown if URL points not to the framework itself.
     */
    private static void checkESciDocLocalURL(final String url) throws InvalidContentException {
        if (!(url.startsWith("/ir/") || url.startsWith("/st/"))) {
            throw new InvalidContentException(
                    "The local URL '" + url + "' does not point into an eSciDoc Core component.");
        }

    }

}