org.bluedolmen.alfresco.marketplace.utils.ModuleManager.java Source code

Java tutorial

Introduction

Here is the source code for org.bluedolmen.alfresco.marketplace.utils.ModuleManager.java

Source

package org.bluedolmen.alfresco.marketplace.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarInputStream;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.rating.RatingService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bluedolmen.alfresco.marketplace.model.MarketPlaceModel;
import org.bluedolmen.marketplace.commons.module.Author;
import org.bluedolmen.marketplace.commons.module.ModuleDescription;
import org.bluedolmen.marketplace.commons.module.ModuleDescriptionFactory;
import org.bluedolmen.marketplace.commons.module.Requirement;
import org.bluedolmen.marketplace.commons.module.Version;
import org.joda.money.Money;

/**
 * Low level common operations dedicated to module management in the Alfresco
 * repository.
 * 
 * @author pajot-b
 * 
 */
public class ModuleManager {

    public static final String TAG_COST_FREE = "#free";
    static final String KEY_CURRENT_MODULE_DESCRIPTION = "mp.currentModuleDescription";
    public static final String FIVE_STAR_SCHEME_NAME = "fiveStarRatingScheme";

    private static final Log logger = LogFactory.getLog(ModuleManager.class);

    private NodeService nodeService;
    private ContentService contentService;
    private RatingService ratingService;
    private TaggingService taggingService;
    private FileFolderUtil fileFolderUtil;

    public static Map<QName, Serializable> createAlfrescoProperties(ModuleDescription moduleDescription,
            boolean includeName) {

        final Map<QName, Serializable> properties = new HashMap<QName, Serializable>(4);

        properties.put(MarketPlaceModel.PROP_NAME, moduleDescription.getName());
        properties.put(MarketPlaceModel.PROP_GROUPID, moduleDescription.getGroupId());
        properties.put(MarketPlaceModel.PROP_ID, moduleDescription.getModuleId());
        properties.put(MarketPlaceModel.PROP_PACKAGING, moduleDescription.getPackaging());
        properties.put(MarketPlaceModel.PROP_CATEGORY, moduleDescription.getCategory());

        final Author author = moduleDescription.getAuthor();
        if (null != author) {
            properties.put(MarketPlaceModel.PROP_AUTHOR, author.getUsername());
        }

        final String description = moduleDescription.getDescription();
        if (StringUtils.isNotBlank(description)) {
            properties.put(ContentModel.PROP_DESCRIPTION, description);
        }

        final Money cost = moduleDescription.getCost();
        if (null != cost) {
            properties.put(MarketPlaceModel.PROP_COST, cost.toString());
        }

        if (includeName) {
            properties.put(ContentModel.PROP_NAME, moduleDescription.getImplicitFileName());
        }

        return properties;

    }

    public class NodeRefModuleDescription implements ModuleDescription {

        private static final long serialVersionUID = 5186031361766189918L;
        private final NodeRef moduleNode;
        private final Map<QName, Serializable> properties;
        private List<String> screenshotIds = null;

        private NodeRefModuleDescription(NodeRef moduleNode) {

            if (null == moduleNode) {
                throw new NullPointerException("The provided module-node is null");
            }

            this.moduleNode = moduleNode;
            properties = nodeService.getProperties(moduleNode);

        }

        void reload() {
            properties.clear();
            properties.putAll(nodeService.getProperties(moduleNode));
            screenshotIds = null;
        }

        @Override
        public Author getAuthor() {

            final String authorName = (String) properties.get(MarketPlaceModel.PROP_AUTHOR);
            if (null == authorName)
                return null;

            final Author author = new Author();
            author.setUsername(authorName);
            return author;

        }

        @Override
        public Money getCost() {

            final String cost = (String) properties.get(MarketPlaceModel.PROP_COST);
            if (StringUtils.isBlank(cost))
                return null;

            return Money.parse(cost);

        }

        @Override
        public String getDescription() {
            return (String) properties.get(ContentModel.PROP_DESCRIPTION);
        }

        @Override
        public String getGroupId() {
            return (String) properties.get(MarketPlaceModel.PROP_GROUPID);
        }

        @Override
        public String getImplicitFileName() {
            return (String) properties.get(ContentModel.PROP_NAME);
        }

        @Override
        public InputStream getLogo() {

            final NodeRef thumbnailNode = getModuleThumbnailNode(moduleNode);
            if (null == thumbnailNode)
                return null;

            final ContentReader reader = contentService.getReader(thumbnailNode, ContentModel.PROP_CONTENT);
            return reader.getContentInputStream();

        }

        @Override
        public String getModuleId() {
            return (String) properties.get(MarketPlaceModel.PROP_ID);
        }

        @Override
        public String getName() {
            return (String) properties.get(MarketPlaceModel.PROP_NAME);
        }

        @Override
        public String getPackaging() {
            return (String) properties.get(MarketPlaceModel.PROP_PACKAGING);
        }

        @Override
        public BigDecimal getRating() {

            float rating = ratingService.getAverageRating(moduleNode, FIVE_STAR_SCHEME_NAME);
            if (-1. != rating) {
                return BigDecimal.valueOf(rating);
            }

            final Float rating_ = (Float) properties.get(MarketPlaceModel.PROP_RATING);
            return BigDecimal.valueOf(null != rating_ ? rating_ : -1);

        }

        @Override
        public Version getVersion() {
            final String version_ = (String) properties.get(MarketPlaceModel.PROP_VERSION);
            return Version.fromString(version_);
        }

        @Override
        public List<String> getScreenshotIds() {

            if (null == screenshotIds) {
                screenshotIds = new ArrayList<String>();
                final List<ChildAssociationRef> screenshotChildren = nodeService.getChildAssocs(moduleNode,
                        MarketPlaceModel.ASSOC_SCREENSHOT, RegexQNamePattern.MATCH_ALL);
                for (final ChildAssociationRef screenshotChild : screenshotChildren) {
                    screenshotIds.add(screenshotChild.getQName().getLocalName());
                }
                screenshotIds = Collections.unmodifiableList(screenshotIds);
            }

            return screenshotIds;

        }

        @Override
        public InputStream getScreenshot(String screenshotId) {

            final NodeRef screenshotNode = getScreenshotNode(moduleNode, screenshotId);
            if (null == screenshotNode)
                return null;

            final ContentReader reader = contentService.getReader(screenshotNode, ContentModel.PROP_CONTENT);
            return reader.getContentInputStream();

        }

        @Override
        public String getCategory() {
            return (String) properties.get(MarketPlaceModel.PROP_CATEGORY);
        }

        @Override
        public List<String> getTags() {
            return taggingService.getTags(moduleNode);
        }

        @Override
        public String getLicenseFileName() {
            throw new UnsupportedOperationException();
        }

        @Override
        public InputStream getLicenseFile() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Requirement getRequirement(String type, String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public List<Requirement> getRequirements(String type, String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public List<Requirement> getRequires() {
            throw new UnsupportedOperationException();
        }

    }

    /**
     * Get the module description w.r.t. meta-data properties of the node.
     * <p>
     * The resulting ModuleDescription can thus be incomplete, however the
     * fundamental properties should be available (groupId, moduleId, version -
     * if available -, name, description)
     * 
     * TODO: We should probably provide an implementation of
     * {@link ModuleDescription} with a nodeRef instead of this...
     * 
     * @param moduleNode
     * @return
     */
    public ModuleDescription getModuleDescriptionFromProperties(NodeRef moduleNode) {

        if (null == moduleNode) {
            throw new NullPointerException("The provided module-node is null");
        }

        return new NodeRefModuleDescription(moduleNode);

    }

    public void checkModule(final NodeRef moduleNode) {

        if (null == moduleNode) {
            throw new NullPointerException("The provided node is null");
        }

        if (!nodeService.exists(moduleNode)) {
            throw new AlfrescoRuntimeException(String.format("The node '%s' does not exist", moduleNode));
        }

        if (!nodeService.hasAspect(moduleNode, MarketPlaceModel.ASPECT_MODULE)) {
            throw new AlfrescoRuntimeException(String.format("The node '%s' is not a valid module", moduleNode));
        }

    }

    public JarInputStream getModuleInputStream(final NodeRef moduleNode) throws IOException {

        if (null == moduleNode) {
            throw new NullPointerException("The provided nodeRef is null");
        }

        final ContentReader contentReader = contentService.getReader(moduleNode, ContentModel.PROP_CONTENT);
        if (!contentReader.exists())
            return null;

        final InputStream inputStream = contentReader.getContentInputStream();
        return new JarInputStream(inputStream);

    }

    public static class InvalidModule extends AlfrescoRuntimeException {

        private static final long serialVersionUID = 7252555805518473053L;
        private final NodeRef nodeRef;

        public InvalidModule(NodeRef nodeRef, String message) {
            super(String.format("[%s] " + message, nodeRef));
            this.nodeRef = nodeRef;
        }

        public InvalidModule(NodeRef nodeRef, String message, Throwable t) {
            super(String.format("[%s] " + message, nodeRef), t);
            this.nodeRef = nodeRef;
        }

        public NodeRef getNodeRef() {
            return nodeRef;
        }

    }

    /**
     * Get the module description from a given node (content).
     * 
     * @param moduleNodeRef
     * @return
     */
    public ModuleDescription getModuleDescriptionFromContent(NodeRef moduleNodeRef) throws InvalidModule {

        final ContentReader reader = contentService.getReader(moduleNodeRef, ContentModel.PROP_CONTENT);
        final InputStream inputStream = reader.getContentInputStream();

        if (null == inputStream) {
            throw new InvalidModule(moduleNodeRef, "The node does not define any content or it is not accessible.");
        }

        try {
            return ModuleDescriptionFactory.createModuleDescription(inputStream);
        } catch (IOException e) {
            throw new InvalidModule(moduleNodeRef, "Cannot get the module-description from the current node");
        } finally {
            IOUtils.closeQuietly(inputStream);
        }

    }

    //   /**
    //    * Get the packaging of a module stored in the repository.
    //    * <p>
    //    * The method mainly relies on properties of the meta-data but may check the
    //    * module-content to get the piece of information if it cannot do otherwise.
    //    * 
    //    * @param moduleNode
    //    * @return
    //    */
    //   public String getPackaging(NodeRef moduleNode) {
    //      
    //      if (!nodeService.hasAspect(moduleNode, MarketPlaceModel.ASPECT_MODULE_DESCRIPTION)) {
    //         throw new InvalidModule(moduleNode, "The module-description is not set on the node");
    //      }
    //      
    //      String packaging = (String) nodeService.getProperty(moduleNode, MarketPlaceModel.PROP_PACKAGING);
    //      if (StringUtils.isBlank(packaging)) { // Probably not possible
    //         logger.warn(String.format("Node '%s' does not define a correct packaging property w.r.t. the module specification.", moduleNode));
    //         final ModuleDescription moduleDescription = getModuleDescriptionFromContent(moduleNode);
    //         packaging = moduleDescription.getPackaging();
    //      }
    //      
    //      if (StringUtils.isBlank(packaging)) {
    //         throw new InvalidModule(moduleNode, "The node does not define a valid packaging.");
    //      }
    //      
    //      return packaging;
    //      
    //   }

    /**
     * Get the version of a module stored in the repository.
     * <p>
     * The method mainly relies on properties of the meta-data but may check the
     * module-content to get the piece of information if it cannot do otherwise.
     * 
     * @param moduleNode
     * @return
     */
    public Version getVersion(NodeRef moduleNode) {

        checkModule(moduleNode);

        final String version_ = (String) nodeService.getProperty(moduleNode, MarketPlaceModel.PROP_VERSION);
        Version version = null;
        if (!StringUtils.isBlank(version_)) { // Probably not possible

            version = new Version(version_);

        } else {

            logger.warn(String.format(
                    "Node '%s' does not define a correct version property w.r.t. the module specification.",
                    moduleNode));
            final ModuleDescription moduleDescription = getModuleDescriptionFromContent(moduleNode);
            version = moduleDescription.getVersion();

            if (null == version) {
                throw new InvalidModule(moduleNode, "The node does not define a valid packaging.");
            }

        }

        return version;

    }

    /**
     * Create a module in the parent directory as a common file with the <code>Module</code> aspect.
     * 
     * @param parentRef
     * @param content
     * @param moduleDescription
     * @return
     */
    public NodeRef createModule(NodeRef parentRef, InputStream content, ModuleDescription moduleDescription) {

        if (null == moduleDescription) {
            throw new NullPointerException(
                    "The provided module-description is invalid. User the file-name variant if you cannot get the complete module-description.");
        }

        if (null != moduleDescription) {
            AlfrescoTransactionSupport.bindResource(KEY_CURRENT_MODULE_DESCRIPTION, moduleDescription);
        }

        final String moduleFileName = moduleDescription.getImplicitFileName();

        return createModule(parentRef, content, moduleFileName);

    }

    /**
     * Create a module given the parent, the input content and the filename.
     * <p>
     * This method is low-level: Using it without checking the existence of a
     * previous version of the module may lead to some kind of duplicate-child
     * exceptions w.r.t. the rules that may execute underneath.
     * 
     * @param parentRef
     * @param content
     *            The stream of the input content. The resource will be closed
     *            unconditionally at the end of the process
     * @param moduleFileName
     * @return
     */
    public NodeRef createModule(NodeRef parentRef, InputStream content, String moduleFileName) {

        if (StringUtils.isBlank(moduleFileName)) {
            throw new IllegalArgumentException("The module id has to be a non-null and non-blank string");
        }

        final NodeRef moduleNodeRef = nodeService
                .createNode(parentRef, ContentModel.ASSOC_CONTAINS, /* assocTypeQName */
                        QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, moduleFileName), /* assocQName */
                        ContentModel.TYPE_CONTENT, /* nodeTypeQName */
                        null)
                .getChildRef();

        // Write the content
        final ContentWriter writer = contentService.getWriter(moduleNodeRef, ContentModel.PROP_CONTENT, true);
        writer.putContent(content);

        nodeService.addAspect(moduleNodeRef, MarketPlaceModel.ASPECT_MODULE, null); // will trigger the rule to update the properties

        return moduleNodeRef;

    }

    public void updateAttachedResources(NodeRef moduleNode, ModuleDescription moduleDescription) {

        updateTags(moduleNode, moduleDescription);
        updateLogo(moduleNode, moduleDescription);
        updateScreenshots(moduleNode, moduleDescription);

    }

    public void updateTags(NodeRef moduleNode, ModuleDescription moduleDescription) {

        final List<String> tags = new ArrayList<String>(moduleDescription.getTags());

        final Money cost = moduleDescription.getCost();
        if (null == cost || cost.isZero()) {
            tags.add(TAG_COST_FREE);
        }

        taggingService.setTags(moduleNode, tags);

    }

    /**
     * Update the module-node extracted from the content.
     * <p>
     * May user the provided module-description for performance/specific reasons.
     * 
     * @param moduleNode
     * @param moduleDescription
     */
    public void updateLogo(NodeRef moduleNode, ModuleDescription moduleDescription) {

        if (null == moduleDescription) {
            moduleDescription = getModuleDescriptionFromContent(moduleNode);
        }

        // Write the logo if one can be found
        final InputStream logoInput = moduleDescription.getLogo();

        if (null == logoInput)
            return;

        /*
         * We create the child as a Thumbnail by easiness:
         * This type is not included in super-type queries and is not archived.
         * This type however needs a mandatory property for the content 
         */

        final NodeRef logoNodeRef = getOrCreateThumbnailNode(moduleNode);
        final ContentWriter writer = contentService.getWriter(logoNodeRef, ContentModel.PROP_CONTENT, true);
        writer.putContent(logoInput);

    }

    private NodeRef getOrCreateThumbnailNode(NodeRef moduleNode) {

        final NodeRef thumbnailNode = getModuleThumbnailNode(moduleNode);
        if (null != thumbnailNode)
            return thumbnailNode;

        // Then create it
        final Map<QName, Serializable> properties = new HashMap<QName, Serializable>();
        properties.put(ContentModel.PROP_CONTENT_PROPERTY_NAME, ContentModel.PROP_CONTENT);
        return nodeService.createNode(moduleNode, MarketPlaceModel.ASSOC_THUMBNAIL,
                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "thumbnail"), ContentModel.TYPE_THUMBNAIL,
                properties).getChildRef();

    }

    public NodeRef getModuleThumbnailNode(NodeRef moduleNode) {

        final List<ChildAssociationRef> thumbnailChildren = nodeService.getChildAssocs(moduleNode,
                MarketPlaceModel.ASSOC_THUMBNAIL, RegexQNamePattern.MATCH_ALL);
        if (thumbnailChildren.isEmpty())
            return null;

        return thumbnailChildren.get(0).getChildRef();

    }

    public void updateScreenshots(NodeRef moduleNode, ModuleDescription moduleDescription) {

        if (null == moduleDescription) {
            moduleDescription = getModuleDescriptionFromContent(moduleNode);
        }

        removeExistingScreenshots(moduleNode);

        for (final String screenshotId : moduleDescription.getScreenshotIds()) {
            createScreenshotChild(moduleNode, moduleDescription, screenshotId);
        }

    }

    private void removeExistingScreenshots(NodeRef moduleNode) {

        final List<ChildAssociationRef> screenshotChildren = nodeService.getChildAssocs(moduleNode,
                MarketPlaceModel.ASSOC_SCREENSHOT, RegexQNamePattern.MATCH_ALL);
        for (final ChildAssociationRef screenshotChild : screenshotChildren) {
            nodeService.removeChildAssociation(screenshotChild);
        }

    }

    private void createScreenshotChild(NodeRef moduleNode, ModuleDescription moduleDescription,
            String screenshotId) {

        final InputStream input = moduleDescription.getScreenshot(screenshotId);
        if (null == input)
            return;

        final Map<QName, Serializable> properties = new HashMap<QName, Serializable>();
        properties.put(ContentModel.PROP_CONTENT_PROPERTY_NAME, ContentModel.PROP_CONTENT);

        final NodeRef screenshotNode = nodeService
                .createNode(moduleNode, MarketPlaceModel.ASSOC_SCREENSHOT,
                        getScreenshotAssociationQName(screenshotId), ContentModel.TYPE_THUMBNAIL, properties)
                .getChildRef();

        final ContentWriter writer = contentService.getWriter(screenshotNode, ContentModel.PROP_CONTENT, true);
        writer.putContent(input);

    }

    public NodeRef getScreenshotNode(NodeRef moduleNode, String screenshotId) {

        final List<ChildAssociationRef> screenshotChildren = nodeService.getChildAssocs(moduleNode,
                MarketPlaceModel.ASSOC_SCREENSHOT, getScreenshotAssociationQName(screenshotId));
        if (screenshotChildren.isEmpty())
            return null;

        return screenshotChildren.get(0).getChildRef();

    }

    private QName getScreenshotAssociationQName(String screenshotId) {

        //      if (screenshotId.contains("/")) {
        //         final int slashIndex = screenshotId.lastIndexOf('/');
        //         screenshotId = screenshotId.substring(slashIndex + 1);
        //      }
        //      if (StringUtils.isBlank(screenshotId)) {
        //         throw new IllegalArgumentException("The screenshot-id cannot be empty nor be ended by a '/' character.");
        //      }

        return QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, screenshotId);

    }

    /**
     * Update module-node properties using the extracted module-description
     * <p>
     * The file-name may be updated too
     * 
     * @param moduleNodeRef
     * @param updateAttachedResources
     */
    public void updateModuleProperties(NodeRef moduleNodeRef, boolean updateAttachedResources) {

        final ModuleDescription moduleDescription = getCachedModuleDescription(moduleNodeRef);

        final Map<QName, Serializable> properties = ModuleManager.createAlfrescoProperties(moduleDescription, true);
        properties.put(MarketPlaceModel.PROP_VERSION, moduleDescription.getVersion().toString());
        nodeService.addProperties(moduleNodeRef, properties);

        if (updateAttachedResources) {
            updateAttachedResources(moduleNodeRef, moduleDescription);
        }

    }

    /**
     * Get the module-description from the transaction if possible else extract it from the node
     * 
     * @param moduleNodeRef
     * @return
     */
    private ModuleDescription getCachedModuleDescription(NodeRef moduleNodeRef) {

        // Try using the one stored in the transaction
        final ModuleDescription moduleDescription = AlfrescoTransactionSupport
                .<ModuleDescription>getResource(KEY_CURRENT_MODULE_DESCRIPTION);
        if (null != moduleDescription)
            return moduleDescription;

        return getModuleDescriptionFromContent(moduleNodeRef);

    }

    public FileFolderUtil getFileFolderUtil() {
        return fileFolderUtil;
    }

    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
        this.contentService = serviceRegistry.getContentService();
        this.nodeService = serviceRegistry.getNodeService();
        this.ratingService = serviceRegistry.getRatingService();
        this.taggingService = serviceRegistry.getTaggingService();
    }

    public void setFileFolderUtil(FileFolderUtil fileFolderUtil) {
        this.fileFolderUtil = fileFolderUtil;
    }

    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

}