de.escidoc.core.common.util.security.helper.InvocationParser.java Source code

Java tutorial

Introduction

Here is the source code for de.escidoc.core.common.util.security.helper.InvocationParser.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.util.security.helper;

import com.sun.xacml.EvaluationCtx;
import com.sun.xacml.attr.StringAttribute;
import de.escidoc.core.common.business.aa.authorisation.AttributeIds;
import de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException;
import de.escidoc.core.common.exceptions.application.missing.MissingAttributeValueException;
import de.escidoc.core.common.exceptions.application.missing.MissingElementValueException;
import de.escidoc.core.common.exceptions.application.missing.MissingMethodParameterException;
import de.escidoc.core.common.exceptions.system.WebserverSystemException;
import de.escidoc.core.common.util.security.persistence.InvocationMapping;
import de.escidoc.core.common.util.security.persistence.MethodMapping;
import de.escidoc.core.common.util.service.UserContext;
import de.escidoc.core.common.util.string.StringUtility;
import de.escidoc.core.common.util.xml.XmlUtility;
import org.apache.commons.collections.map.LRUMap;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Roland Werner (Accenture)
 */
public class InvocationParser {

    /**
     * Pattern used to detect the place holder for indices in the defined xpath.
     */
    private static final Pattern PATTERN_INDEXED = Pattern.compile(InvocationMapping.INDEXED_PATTERN);

    /**
     * Pattern used to detect sub-resource-attributes in invocation-mappings.
     */
    private static final Pattern PATTERN_SUBRESOURCE = Pattern.compile(InvocationMapping.SUBRESOURCE_PATTERN);

    private static final Matcher matcherSubresource = PATTERN_SUBRESOURCE.matcher("");

    private static final int CACHE_SIZE = 20;

    private final DocumentCache documentCache = new DocumentCache(CACHE_SIZE);

    /**
     * Cache for xml <code>Document</code> objects.<br> This cache is used to avoid multiple parsing of the same
     * document. It provides an method to retrieve a document with creation of not found documents. It uses a
     * <code>LRUMap</code> that is synchronized (via {@link Collections}.synchronizedMap({@link Map})).
     *
     * @author Torsten Tetteroo
     */
    private static final class DocumentCache {

        private final Map<Object, Document> map;

        /**
         * Creates cache of specified size.
         *
         * @param size The number of elements in the cache.
         */
        @SuppressWarnings("unchecked")
        private DocumentCache(final int size) {

            this.map = Collections.synchronizedMap(new LRUMap(size));
        }

        /**
         * Retrieves the document for the provided document data.<br> If it does not exist in the cache, it will be
         * created from the document data and saved.
         *
         * @param documentData The object to get the xml document for, or <code>null</code> in case of an error.
         * @return Returns the xml <code>Document</code> object representing the provided xml data.
         * @throws IOException                  Thrown in case of an i/o error.
         * @throws ParserConfigurationException Thrown in case of an error in parser configuration
         * @throws SAXException                 Thrown in case of a parse error
         * @throws java.io.UnsupportedEncodingException
         */
        public Document retrieveDocument(final Object documentData)
                throws IOException, ParserConfigurationException, SAXException, UnsupportedEncodingException {

            Document document = map.get(documentData);
            if (document == null) {
                final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
                document = builder.parse(
                        new ByteArrayInputStream(((String) documentData).getBytes(XmlUtility.CHARACTER_ENCODING)));
                map.put(documentData, document);
            }

            return document;
        }
    }

    /**
     * Builds a {@link List} of {@link Map} objects that are holding the attributes for a authorization request to the
     * PDP from the provided methodName and arguments, and from the stored user context.<br> This method delegates to
     * <code>buildRequestsList(Object MethodMapping, boolean)</code>
     *
     * @param arguments     The arguments
     * @param methodMapping The method mappings to use
     * @return The generated {@link List} of {@link Map} objects.
     * @throws MissingElementValueException   Thrown if an argument does not contain a mandatory element.
     * @throws MissingAttributeValueException Thrown if an argument does not contain a mandatory attribute .
     * @throws MissingMethodParameterException
     *                                        Thrown if an argument has not been provided but is needed for
     *                                        authorization.
     * @throws de.escidoc.core.common.exceptions.system.WebserverSystemException
     * @throws de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException
     */
    public List<Map<String, String>> buildRequestsList(final Object[] arguments, final MethodMapping methodMapping)
            throws MissingMethodParameterException, MissingAttributeValueException, MissingElementValueException,
            WebserverSystemException, XmlCorruptedException {

        return buildRequestsList(arguments, methodMapping, true);
    }

    /**
     * Builds a {@link List} of {@link Map} objects that are holding the attributes for a authorization request to the
     * PDP from the provided methodName and arguments, and from the stored user context.<br> This method delegates to
     * <code>buildRequestsList(Object MethodMapping, boolean)</code>
     *
     * @param argument      The arguments
     * @param methodMapping The method mappings to use
     * @return The generated {@link List} of {@link Map} objects.
     * @throws MissingElementValueException   Thrown if an argument does not contain a mandatory element.
     * @throws MissingAttributeValueException Thrown if an argument does not contain a mandatory attribute .
     * @throws MissingMethodParameterException
     *                                        Thrown if an argument has not been provided but is needed for
     *                                        authorization.
     * @throws de.escidoc.core.common.exceptions.system.WebserverSystemException
     * @throws de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException
     */
    public List<Map<String, String>> buildRequestsList(final Object argument, final MethodMapping methodMapping)
            throws MissingMethodParameterException, MissingAttributeValueException, MissingElementValueException,
            WebserverSystemException, XmlCorruptedException {

        return buildRequestsList(argument, methodMapping, false);
    }

    /**
     * Builds a {@link List} of {@link Map} objects that are holding the attributes for a authorization request to the
     * PDP from the provided methodName and arguments, and from the stored user context.
     *
     * @param arguments     The arguments
     * @param methodMapping The method mappings to use
     * @param isArray       Flag that indicates that the given arguments parameter is an array (<code>true</code>) or
     *                      not (<code>false</code>).
     * @return The generated {@link List} of {@link Map} objects.
     * @throws MissingElementValueException   Thrown if an argument does not contain a mandatory element.
     * @throws MissingAttributeValueException Thrown if an argument does not contain a mandatory attribute .
     * @throws MissingMethodParameterException
     *                                        Thrown if an argument has not been provided but is needed for
     *                                        authorization.
     * @throws de.escidoc.core.common.exceptions.system.WebserverSystemException
     * @throws de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException
     */
    private List<Map<String, String>> buildRequestsList(final Object arguments, final MethodMapping methodMapping,
            final boolean isArray) throws MissingMethodParameterException, MissingAttributeValueException,
            MissingElementValueException, WebserverSystemException, XmlCorruptedException {

        final Set<InvocationMapping> invocationMappings = methodMapping.getInvocationMappings();

        final List<Map<String, String>> requests = new ArrayList<Map<String, String>>();

        // for each resource, loop will be left in case of IndexOutOfBounds
        // exception
        int index = 0;
        do {
            final Map<String, String> request = new HashMap<String, String>();
            try {
                // setup subject
                String id = UserContext.getId();
                if (id == null) {
                    id = UserContext.ANONYMOUS_IDENTIFIER;
                }
                request.put(AttributeIds.URN_SUBJECT_ID, id);

                // setup resource
                request.putAll(setupResourceAttributes(arguments, invocationMappings, isArray, index));

                // setup action
                request.put(AttributeIds.URN_ACTION_ID, methodMapping.getActionName());
            } catch (final IndexOutOfBoundsException e) {
                break;
            }

            requests.add(request);
            index++;
        } while (!methodMapping.isSingleResource());

        return requests;
    }

    /**
     * Creates XACML resource attributes for the provided data.<br>
     * <p/>
     * For this the arguments' data referenced by the provided invocation mappings are relevant. Each of these
     * referenced objects will be transformed into an XACML resource attribute.
     *
     * @param arguments          The arguments of the method call.
     * @param invocationMappings The invocation mappings for that the request shall be generated.
     * @param isArray            Flag that indicates that the given arguments parameter is an array (<code>true</code>)
     *                           or not (<code>false</code>).
     * @param index              The index of the current object.
     * @return Returns a <code>Map</code> from attribute urn to attribute value defining the resource attributes.
     * @throws MissingMethodParameterException
     *                                        Thrown if an invocation mapping references an argument that is set to
     *                                        <code>null</code>.
     * @throws MissingAttributeValueException Thrown if an invocation mapping references an attribute in an argument
     *                                        that holds XML data but the attribute cannot be found.
     * @throws MissingElementValueException   Thrown if an invocation mapping references an element in an argument that
     *                                        holds XML data but the element cannot be found.
     * @throws WebserverSystemException       Thrown if there is a problem with an invocation mapping.
     * @throws de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException
     */
    private Map<String, String> setupResourceAttributes(final Object arguments,
            final Iterable<InvocationMapping> invocationMappings, final boolean isArray, final int index)
            throws MissingMethodParameterException, MissingAttributeValueException, MissingElementValueException,
            WebserverSystemException, XmlCorruptedException {

        if (arguments == null || invocationMappings == null) {
            return new HashMap<String, String>();
        }

        final Map<String, String> resourceAttributes = new HashMap<String, String>();
        boolean resourceIdProvided = false;
        boolean subresourceIdProvided = false;

        // for each invocation mapping...
        for (final InvocationMapping invocationMapping : invocationMappings) {
            // FIXME: resolve this
            final StringAttribute value = getValueForInvocationMapping(arguments, isArray, index,
                    invocationMapping);

            // and put the resource attribute in the Vector
            if (value != null) {
                final String attributeId = invocationMapping.getAttributeId();
                if (attributeId.equals(EvaluationCtx.RESOURCE_ID)) {
                    // found the resource ID
                    resourceIdProvided = true;
                } else if (matcherSubresource.reset(attributeId).matches()) {
                    resourceAttributes.put(AttributeIds.URN_SUBRESOURCE_ATTR, value.getValue());
                    subresourceIdProvided = true;
                }
                resourceAttributes.put(attributeId, value.getValue());
            }
        }

        // we need the resource-id. If not provided, create empty element
        if (!resourceIdProvided) {
            resourceAttributes.put(EvaluationCtx.RESOURCE_ID, "");
        }

        // we need the subresource-id. If not provided, create empty element
        if (!subresourceIdProvided) {
            resourceAttributes.put(AttributeIds.URN_SUBRESOURCE_ATTR, "");
        }

        return resourceAttributes;
    }

    /**
     * Gets the {@link StringAttribute} for the provided values.
     *
     * @param arguments         The arguments of the method call.
     * @param isArray           Flag that indicates that the given arguments parameter is an array (<code>true</code>)
     *                          or not (<code>false</code>).
     * @param index             The index of the current object.
     * @param invocationMapping The {@link InvocationMapping} to get the value for.
     * @return Returns a {@link StringAttribute} with the value for the invocation mapping.
     * @throws MissingMethodParameterException
     *                                  Thrown if a mandatory method parameter is not provided.
     * @throws WebserverSystemException Thrown in case of an internal error.
     * @throws de.escidoc.core.common.exceptions.application.invalid.XmlCorruptedException
     */
    private StringAttribute getValueForInvocationMapping(final Object arguments, final boolean isArray,
            final int index, final InvocationMapping invocationMapping)
            throws WebserverSystemException, MissingMethodParameterException, XmlCorruptedException {

        // set the value
        final StringAttribute value;
        if (invocationMapping.getMappingType() == InvocationMapping.VALUE_MAPPING) {
            // fetch the value from inside the invocation mapping
            value = new StringAttribute(invocationMapping.getValue());
        } else {
            // Get the current object addressed by the invocation mapping
            final Object currentObject;
            if (isArray) {
                currentObject = ((Object[]) arguments)[invocationMapping.getPosition()];
            } else {
                if (invocationMapping.getPosition() != 0) {
                    throw new WebserverSystemException("Invocation mapping error. Position "
                            + invocationMapping.getPosition() + " invalid for single argument, must be 0. [id="
                            + invocationMapping.getId() + ']');
                }
                currentObject = arguments;
            }

            // assert the addressed object has been provided
            if (currentObject == null) {
                throw new MissingMethodParameterException("The parameter at specified "
                        + "position must be provided" + (invocationMapping.getPosition() + 1));
            }
            if (invocationMapping.getMappingType() == InvocationMapping.SIMPLE_ATTRIBUTE_MAPPING) {
                value = new StringAttribute(currentObject.toString());
            } else if (invocationMapping.getMappingType() == InvocationMapping.XML_ATTRIBUTE_MAPPING
                    || invocationMapping.getMappingType() == InvocationMapping.OPTIONAL_XML_ATTRIBUTE_MAPPING) {
                // fetch the value from XML document
                final Document document;
                try {
                    document = documentCache.retrieveDocument(currentObject);
                } catch (final SAXException e) {
                    throw new XmlCorruptedException(
                            StringUtility.format("Parsing of provided XML data failed. ", e.getMessage()), e);
                } catch (final Exception e) {
                    throw new WebserverSystemException("Internal error. Parsing of XML failed.", e);
                }

                String path = invocationMapping.getPath();
                boolean extractObjidNeeded = false;
                if (path.startsWith("extractObjid:")) {
                    path = path.substring("extractObjid:".length());
                    extractObjidNeeded = true;
                }
                final String xpath = PATTERN_INDEXED.matcher(path).replaceAll("[" + (index + 1) + ']');
                final NodeList nodeList;
                try {
                    nodeList = XPathAPI.selectNodeList(document, xpath);
                } catch (final TransformerException e) {
                    throw new WebserverSystemException(
                            StringUtility.format("Invocation mapping error. Xpath invalid?", xpath, index,
                                    invocationMapping.getId()),
                            e);
                }

                if (nodeList == null || nodeList.getLength() == 0) {
                    if (index > 0) {
                        throw new IndexOutOfBoundsException();
                    } else if (invocationMapping.getMappingType() == InvocationMapping.XML_ATTRIBUTE_MAPPING) {
                        throw new XmlCorruptedException(
                                StringUtility.format("Expected value not found " + "in provided XML data ", xpath));
                    } else {
                        // skip undefined optional attribute by setting
                        // the value to null.
                        value = null;
                    }
                } else {
                    int length = 1;
                    if (invocationMapping.isMultiValue()) {
                        length = nodeList.getLength();
                    }
                    final Collection<String> values = new HashSet<String>();
                    for (int i = 0; i < length; i++) {
                        final Node node = nodeList.item(i);
                        String tmpValue = null;
                        if (node.getFirstChild() != null) {
                            tmpValue = node.getFirstChild().getNodeValue();
                        } else {
                            tmpValue = "";
                        }
                        if (tmpValue != null) {
                            if (extractObjidNeeded) {
                                tmpValue = XmlUtility.getIdFromURI(tmpValue);
                            }
                            values.add(tmpValue.trim());
                        }
                    }
                    if (values.isEmpty()) {
                        value = null;
                    } else {
                        final StringBuilder valueBuf = new StringBuilder("");
                        for (final String val : values) {
                            if (!val.isEmpty()) {
                                if (valueBuf.length() > 0) {
                                    valueBuf.append(' ');
                                }
                                valueBuf.append(val);
                            }
                        }
                        value = new StringAttribute(valueBuf.toString());
                    }
                }
            } else {
                throw new WebserverSystemException(StringUtility.format("Unsupported invocation mapping type",
                        invocationMapping.getMappingType(), invocationMapping.getId()));
            }
        }
        return value;
    }
}