All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.granite.comments.AbstractCommentingProvider Maven / Gradle / Ivy

There is a newer version: 6.5.21
Show newest version
/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2013 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.granite.comments;

import java.io.InputStream;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;

import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.util.Text;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.osgi.annotation.versioning.ConsumerType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.comments.internal.Util;

/**
 * The AbstractCommentingProvider provides a default implementation for storing {@link Comment}s and {@link
 * CommentCollection}s. {@link CommentingProvider}s are recommended to extend this abstract implementation and override
 * where necessary.
 */
@ConsumerType
public abstract class AbstractCommentingProvider implements CommentingProvider {

    private final Logger log = LoggerFactory.getLogger(AbstractCommentingProvider.class);

    private final Predicate COMMENT_RESOURCE_TYPE_PREDICATE = new Predicate() {
        @Override
        public boolean evaluate(Object o) {
            final Resource resource = (Resource) o;
            return ResourceUtil.isA(resource, getCommentResourceType());
        }
    };

    /**
     * The name of the node holding comment collections below a target.
     */
    public static final String RELATIVE_TARGET_ROOT = "comments";

    /**
     * The name of the property holding optional annotation data
     */
    public static final String PN_ANNOTATIONDATA = "annotationData";

    /**
     * The property containing the optional creator of a comment
     */
    public static final String PN_AUTHOR = "author";

    /**
     * The name of the property holding a comment's message ("comment").
     */
    public static final String PN_MESSAGE = "jcr:description";

    public static final String JCR_CREATED_BY = "jcr:createdBy";
    public static final String SLING_RESOURCE_TYPE = "sling:resourceType";

    /**
     * Create the root of a {@link CommentCollection} for the given target {@link Resource}. Node creation may be
     * overridden via {@link #createCollectionNode(String, javax.jcr.Session)}. If not already present on the node, this
     * method also will set the {@link #JCR_CREATED_BY} and {@link JcrConstants#JCR_CREATED} properties. The last
     * modified mixin will maintain the last modified date. Custom properties may be set on the collection node via
     * {@link #customizeCollectionNode(Resource, javax.jcr.Node)}.
     *
     * @param target The target resource for which to create a collection root.
     *
     * @return The newly created resource representing the collection root.
     *
     * @throws CommentException If a collection already exists for the given target, or upon encountering an error
     *                          writing to the repository.
     */
    public final Resource createCollectionResource(final Resource target) {

        final String rootPath = getCollectionResourcePath(target);
        final Session session = target.getResourceResolver().adaptTo(Session.class);
        try {
            if (session.itemExists(rootPath)) {
                throw new CommentException("Collection already exists: " + rootPath);
            }
            final Node collectionRoot = createCollectionNode(rootPath, session);
            if (StringUtils.isNotBlank(getCollectionResourceType())) {
                collectionRoot.setProperty(SLING_RESOURCE_TYPE, getCollectionResourceType());
            }

            collectionRoot.addMixin(NodeType.MIX_LAST_MODIFIED);

            // these properties may already have been defined by a node type used in #createCollectionNode
            if (!collectionRoot.hasProperty(JcrConstants.JCR_CREATED)) {
                collectionRoot.setProperty(JcrConstants.JCR_CREATED, Calendar.getInstance());
            }
            if (!collectionRoot.hasProperty(JCR_CREATED_BY)) {
                collectionRoot.setProperty(JCR_CREATED_BY, session.getUserID());
            }

            customizeCollectionNode(target, collectionRoot);

            session.save();
            return target.getResourceResolver().getResource(collectionRoot.getPath());
        } catch (RepositoryException e) {
            log.error("error while creating collection root for target [{}]: ", target.getPath(), e);
            throw new CommentException("Could not create collection root for target: " + target.getPath(), e);
        }
    }

    /**
     * Add a {@link Comment} resource to the given {@link CommentCollection} resource, set the given
     * message on the comment. Comment node creation may be overridden via {@link
     * #createCommentNode(String, javax.jcr.Node, javax.jcr.Session)}. This method will forcibly set a {@link
     * #SLING_RESOURCE_TYPE} property (if defined by {@link #getCommentResourceType()} ()}), the {@link #PN_MESSAGE}
     * property. If not already present on the node, this method also will set the {@link #JCR_CREATED_BY} and {@link
     * JcrConstants#JCR_CREATED} properties. Custom properties may be set on the comment resource via {@link
     * #customizeCommentNode(Resource, javax.jcr.Node)} (javax.jcr.Node)}.
     *
     * @param collectionResource The collection within which to create the comment.
     * @param message            The message to store in the comment.
     * @param author             The author to store in the comment.
     * @param annotationData     The annotation data to store in the comment.
     *
     * @return The newly created comment {@link Resource}.
     *
     * @throws CommentException Upon encountering an error writing to the repository.
     */
    public final Resource createCommentResource(final Resource collectionResource,
                                                final String message,
                                                final String author,
                                                final String annotationData) {
        try {
            final ResourceResolver resolver = collectionResource.getResourceResolver();
            final Session session = resolver.adaptTo(Session.class);
            final Node collectionNode = session.getNode(collectionResource.getPath());
            final String commentPath = getCommentResourcePath(collectionResource, message);
            final Node commentNode = createCommentNode(commentPath, collectionNode, session);
            if (StringUtils.isNotBlank(getCommentResourceType())) {
                commentNode.setProperty(SLING_RESOURCE_TYPE, getCommentResourceType());
            }

            commentNode.setProperty(PN_MESSAGE, message);

            customizeCommentNode(collectionResource, commentNode);

            final Calendar now = Calendar.getInstance();
            commentNode.addMixin(NodeType.MIX_LAST_MODIFIED);
            if (!commentNode.hasProperty(JcrConstants.JCR_CREATED)) {
                commentNode.setProperty(JcrConstants.JCR_CREATED, now);
            }

            if (!commentNode.hasProperty(JCR_CREATED_BY)) {
                commentNode.setProperty(JCR_CREATED_BY, session.getUserID());
            }

            if (StringUtils.isNotBlank(author)) {
                commentNode.setProperty(PN_AUTHOR, author);
            }

            if (StringUtils.isNotBlank(annotationData)) {
                commentNode.setProperty(PN_ANNOTATIONDATA, annotationData);
            }

            collectionNode.setProperty(JcrConstants.JCR_LASTMODIFIED, now);

            session.save();
            return resolver.getResource(commentNode.getPath());
        } catch (RepositoryException e) {
            log.error("error while creating comment for collection [{}]: ", collectionResource.getPath(), e);
            throw new CommentException("Could not x for collection: " + collectionResource.getPath(), e);
        }
    }

    /**
     * Returns an {@link Iterator} of {@link Resource}s, with each resource representing a {@link Comment} of the given
     * {@link CommentCollection}.
     *
     * @param collectionResource The {@link CommentCollection} for which to retrieve the comment resources.
     *
     * @return The iterator containing the comment resources, or an empty iterator if no comments are present.
     *
     * @throws CommentException Upon encountering an error retrieving the comments.
     */
    public Iterator getCommentResources(Resource collectionResource) {
        final Iterator children = collectionResource.listChildren();
        return new FilterIterator(children, COMMENT_RESOURCE_TYPE_PREDICATE);
    }

    /**
     * Remove (delete) the given {@link Comment} from the repository.
     *
     * @param resource The comment resource to delete.
     *
     * @throws CommentException Upon encountering an error writing to the repository.
     */
    public void removeCommentResource(final Resource resource) {
        try {
            final Session session = resource.getResourceResolver().adaptTo(Session.class);
            final Node node = session.getNode(resource.getPath());
            node.remove();
            session.save();
        } catch (RepositoryException e) {
            log.error("error while removing comment [{}]: ", resource.getPath(), e);
            throw new CommentException("Could not remove comment: " + resource.getPath(), e);
        }
    }

    /**
     * Remove (delete) the given {@link CommentCollection} from the repository.
     *
     * @param resource The collection to delete.
     *
     * @throws CommentException Upon encountering an error writing to the repository.
     */
    public void removeCollectionResource(Resource resource) {
        try {
            final ResourceResolver resolver = resource.getResourceResolver();
            if (null != resolver.getResource(resource.getPath())) {
                final Session session = resolver.adaptTo(Session.class);
                session.removeItem(resource.getPath());
                session.save();
            }
        } catch (RepositoryException e) {
            log.error("error while removing collection [{}]: ", resource.getPath(), e);
            throw new CommentException("Could not remove collection: " + resource.getPath(), e);
        }
    }

    /**
     * Add an attachment to the given {@link Comment}. The attachment is represented by the given name
     * (file name), an {@link InputStream} and the mime type, all of which are mandatory parameters. The relative path
     * with which the attachment is created can be customized via {@link #getAttachmentResourcePath(String)}, also
     * arbitrary properties can be set on the attachment's content node via {@link #customizeAttachmentNode(Resource,
     * javax.jcr.Node)}.
     *
     * @param commentResource The comment to which to add the attachment.
     * @param name            The name (file name) for the attachment.
     * @param inputStream     The input stream containing the file data of the attachment.
     * @param mimeType        The mime type of the attachment data.
     *
     * @return The newly created attachment resource.
     *
     * @throws CommentException Upon encountering an error writing to the repository.
     */
    public final Resource createAttachmentResource(Resource commentResource,
                                                   String name,
                                                   InputStream inputStream,
                                                   String mimeType) {
        try {
            final ResourceResolver resolver = commentResource.getResourceResolver();
            final Session session = resolver.adaptTo(Session.class);
            final Calendar time = Calendar.getInstance();
            final Node node = session.getNode(commentResource.getPath());
            final Node attachment = JcrUtils.getOrCreateUniqueByPath(node,
                                                                     getAttachmentResourcePath(name),
                                                                     JcrConstants.NT_FILE);
            final Node content = attachment.addNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_UNSTRUCTURED);

            content.setProperty(JcrConstants.JCR_CREATED, time);
            content.setProperty(JCR_CREATED_BY, node.getSession().getUserID());
            content.setProperty(JcrConstants.JCR_LASTMODIFIED, time);
            content.setProperty(JcrConstants.JCR_MIMETYPE, mimeType);
            content.setProperty(JcrConstants.JCR_DATA, node.getSession().getValueFactory().createBinary(inputStream));

            customizeAttachmentNode(commentResource, content);

            session.save();

            return resolver.getResource(attachment.getPath());
        } catch (RepositoryException e) {
            log.error("error while creating attachment for comment [{}]: ", commentResource.getPath(), e);
            throw new CommentException("Could not create attachment for comment: " + commentResource.getPath(),
                                       e);
        }
    }

    /**
     * Return the attachment of the given {@link Comment} as identified by its given name (file name).
     *
     * @param commentResource The comment from which to get the attachment.
     * @param name            The name of the attachment.
     *
     * @return The {@link Resource} representing the attachment, or null if no attachment with the given
     *         name was found.
     *
     * @throws CommentException Upon encountering an error retrieving the attachment.
     */
    public Resource getAttachmentResource(Resource commentResource, String name) {
        final Resource child = commentResource.getChild(getAttachmentResourcePath(name));
        return (ResourceUtil.isA(child, JcrConstants.NT_FILE)) ? child : null;
    }

    /**
     * Remove (delete) the attachment identified by the given name from the given {@link Comment}.
     *
     * @param commentResource The comment from which to remove the attachment.
     * @param name            The name of the attachment to remove.
     *
     * @throws CommentException Upon encountering an error writing to the repository.
     */
    public void removeAttachmentResource(Resource commentResource, String name) {
        try {
            final Resource attachment = commentResource.getChild(getAttachmentResourcePath(name));
            if (null != attachment) {
                final Session session = attachment.getResourceResolver().adaptTo(Session.class);
                session.removeItem(attachment.getPath());
                session.save();
            }
        } catch (RepositoryException e) {
            log.error("error while removing attachment from comment [{}]: ", commentResource.getPath(), e);
            throw new CommentException("Could not remove attachment from comment: " + commentResource.getPath(),
                                       e);
        }
    }

    /**
     * Return a {@link Map} containing the given {@link Comment}s attachments. The map key is the attachment's name
     * (file name), the map value holds the {@link Resource} representing the attachment.
     *
     * @param commentResource The comment for which to retrieve the attachments.
     *
     * @return The map of attachments, or an empty map if no attachments are present.
     *
     * @throws CommentException Upon encountering an error retrieving the attachments.
     */
    public final Map getAttachmentMap(Resource commentResource) throws CommentException {
        final Map attachments = new HashMap();
        final Iterator iterator = getAttachments(commentResource);
        while (iterator.hasNext()) {
            Resource child = iterator.next();
            if (ResourceUtil.isA(child, JcrConstants.NT_FILE)) {
                attachments.put(child.getName(), child);
            }
        }
        return attachments;
    }

    /**
     * Returns the {@link Resource} representing the root of the collection specific to the given target,
     * or null if no collection exists for this target.
     *
     * @param target The target resource for which to retrieve the collection root.
     *
     * @return The resource repesenting the collection, or null if none is present.
     *
     * @throws CommentException Upon encountering an error retrieving the collection root.
     */
    protected final Resource getCollectionResource(Resource target) throws CommentException {
        return target.getResourceResolver().getResource(getCollectionResourcePath(target));
    }

    /**
     * Creates the node of the collection. This method is called by {@link #createCollectionResource(org.apache.sling.api.resource.Resource)}
     * while creating the resource and allows for customization of creation of the node that serves as the bases for the
     * resource representing this collection. {@link #createCollectionResource(org.apache.sling.api.resource.Resource)}
     * Note that {@link #createCollectionResource(org.apache.sling.api.resource.Resource)} will set mandatory properties
     * of its own and that this method should not set any properties on its own, but rather later via {@link
     * #customizeCollectionNode(Resource, javax.jcr.Node)}. Overriding implementations can use the session to create a
     * node. The save-call is done by the calling method and is not required here.
     *
     * @param rootPath The complete path of the node to be created (as defined by {@link
     *                 #getCollectionResourcePath(org.apache.sling.api.resource.Resource)}.
     * @param session  The session to write to the repository with.
     *
     * @return The newly created node representing the collection.
     */
    protected Node createCollectionNode(final String rootPath, final Session session) {
        try {
            Node root = null;
            String path = rootPath;
            while (root == null && StringUtils.isNotBlank(path)) {
                path = Text.getRelativeParent(path, 1);
                if (session.nodeExists(path)) {
                    root = session.getNode(path);
                }
            }
            if (root != null) {
                String relPath = StringUtils.removeStart(rootPath, path + '/');
                return JcrUtils.getOrCreateByPath(root, relPath, false, JcrConstants.NT_UNSTRUCTURED, JcrConstants.NT_UNSTRUCTURED, false);
            } else {
                throw new CommentException("Unable to access parent nodes of new collection");
            }
        } catch (RepositoryException e) {
            throw new CommentException("Error creating collection root at: " + rootPath, e);
        }
    }

    /**
     * Creates the node of the comment below the given collectionNode. This method is called by {@link
     * #createCommentResource(Resource, String, String, String)} while creating the resource and allows for
     * customization of creation of the node that serves as the bases for the resource representing the comment.
     * Overriding implementations can use the session to create a node. The save-call is done by the calling method and
     * is not required here.
     *
     * @param commentPath    The relative path of the node to be created (as defined by {@link
     *                       #getCommentResourcePath(org.apache.sling.api.resource.Resource, String)}.
     * @param collectionNode The node of the comment's collection node.
     * @param session        The session to write to the repository with.
     *
     * @return The newly created node representing the collection.
     */
    protected Node createCommentNode(final String commentPath, final Node collectionNode, final Session session) {
        try {
            return JcrUtils.getOrCreateUniqueByPath(collectionNode, commentPath, JcrConstants.NT_UNSTRUCTURED);
        } catch (RepositoryException e) {
            throw new CommentException("Error creating comment node at: " + commentPath, e);
        }
    }

    /**
     * Allows customization of the given collection {@link Node}, by e.g. setting arbitrary properties. Overriding
     * implementations need not save the session, this is done in the calling {@link
     * #createCollectionResource(org.apache.sling.api.resource.Resource)} (org.apache.sling.api.resource.Resource,
     * String)}. The default implementation does nothing.
     *
     * @param target         The resource representing the target of the collection.
     * @param collectionNode The node representing the collection.
     */
    protected void customizeCollectionNode(final Resource target, final Node collectionNode) {
    }

    /**
     * Allows customization of the given comment {@link Node}, by e.g. setting arbitrary properties. Overriding
     * implementations need not save the session, this is done in the calling {@link #createCommentResource(Resource,
     * String, String, String)} (org.apache.sling.api.resource.Resource, String)}. The default implementation does
     * nothing.
     *
     * @param collectionResource The resource representing the collection.
     * @param commentNode        The node representing the comment.
     */
    protected void customizeCommentNode(final Resource collectionResource, final Node commentNode) {
    }

    /**
     * Allows customization of the given attachment content {@link Node}, by e.g. setting arbitrary properties.
     * Overriding implementations need not save the session, this is done in the calling {@link
     * #createCommentResource(Resource, String, String, String)} (org.apache.sling.api.resource.Resource, String)}. The
     * default implementation does nothing.
     *
     * @param commentResource       The resource representing the comment this attachment belongs to.
     * @param attachmentContentNode The node representing the attachment content.
     */
    protected void customizeAttachmentNode(final Resource commentResource, final Node attachmentContentNode) {
    }

    /**
     * Calculates the path of the root of a {@link CommentCollection}. This may be overriden. The default implementation
     * appends "jcr:content" (if the given target has such a child node) and a "/" and {@link #RELATIVE_TARGET_ROOT} to
     * the path of the target.
     *
     * @param target The target for which to calculate the path of the collection.
     *
     * @return The path.
     */
    protected String getCollectionResourcePath(final Resource target) {
        final Resource contentResource = target.getChild(JcrConstants.JCR_CONTENT);
        final String path = (null != contentResource) ? contentResource.getPath() : target.getPath();
        return path + "/" + RELATIVE_TARGET_ROOT;
    }

    /**
     * Calculates the relative path of a comment below a collection. The default simply transform the given
     * message into a valid node name and returns it.
     *
     * @param collectionResource The collection within which the comment belongs.
     * @param message            The comment message.
     *
     * @return The path.
     */
    protected String getCommentResourcePath(final Resource collectionResource, final String message) {
        return Util.createValidName(message);
    }

    /**
     * Calculates the relative path of an attachment, based on the given name, below a comment. The default simply
     * returns the given name.
     *
     * @param name The attachment name (e.g. filename)
     *
     * @return The path.
     */
    protected String getAttachmentResourcePath(final String name) {
        return name;
    }

    /**
     * Provides an iterator of resources representing attachments of a given comment (resource). The default simply
     * lists the children of the given comment resource.
     *
     * @param commentResource The resource representing the comment to which the attachment belongs.
     *
     * @return The iterator of attachment resources.
     */
    protected Iterator getAttachments(final Resource commentResource) {
        return commentResource.listChildren();
    }

    /**
     * Returns an optional resource type to set for newly created collections.
     *
     * @return The resource type or null if none shall be set.
     */
    public abstract String getCollectionResourceType();

    /**
     * Returns an optional resource type to set for newly created comments.
     *
     * @return The resource type or null if none shall be set.
     */
    public abstract String getCommentResourceType();
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy