com.surevine.alfresco.webscript.gsa.getallitems.GSANodePropertyServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.surevine.alfresco.webscript.gsa.getallitems.GSANodePropertyServiceImpl.java

Source

/*
 * Copyright (C) 2008-2010 Surevine Limited.
 *   
 * Although intended for deployment and use alongside Alfresco this module should
 * be considered 'Not a Contribution' as defined in Alfresco'sstandard contribution agreement, see
 * http://www.alfresco.org/resource/AlfrescoContributionAgreementv2.pdf
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package com.surevine.alfresco.webscript.gsa.getallitems;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.Base64;

import com.surevine.alfresco.repo.profile.UserProfileModel;
import com.surevine.alfresco.webscript.gsa.exception.GSAProcessingException;
import com.surevine.alfresco.webscript.gsa.getallitems.NodeRefTypeResolver.NodeRefType;

/**
 * Implementation class for GSANodePropertyService.
 * 
 * This class is responsible for interfacing between the alfresco repository itself and the rest of the GSA service
 * @author simonw
 *
 */
public class GSANodePropertyServiceImpl implements GSANodePropertyService {

    /**
     * Placeholder name used for anonymity purposes.
     */
    private static final String OFFICER = "officer";

    private NodeService _nodeService;
    private ContentService _contentService;
    private PersonService _personService;

    private static final String NAMESPACE_SV = "http://www.alfresco.org/model/enhancedSecurity/0.3";
    private static final QName NOD_QN = QName.createQName(NAMESPACE_SV, "nod");
    private static final QName PM_QN = QName.createQName(NAMESPACE_SV, "pm");
    private static final QName OG_QN = QName.createQName(NAMESPACE_SV, "openMarkings");
    private static final QName CG_QN = QName.createQName(NAMESPACE_SV, "closedMarkings");
    private static final QName ORG_QN = QName.createQName(NAMESPACE_SV, "organisations");
    private static final QName NATN_QN = QName.createQName(NAMESPACE_SV, "nationalityCaveats");
    private static final QName FREEFORM_QN = QName.createQName(NAMESPACE_SV, "freeFormCaveats");

    /**
     * String within an item's path immediatley preceding the name of the site the item is in
     */
    private static final String SITE_PART_OF_PATH = "{http://www.alfresco.org/model/site/1.0}sites/{http://www.alfresco.org/model/content/1.0}";

    private static final Log _logger = LogFactory.getLog(GSANodePropertyServiceImpl.class);

    /**
     * Profiles don't have security labels, so we return a constant value informed by the values of these
     * (injected) fields
     */
    private String _profileNod;
    private String _profilePM;
    private Collection<String> _profileNatn;
    private int _maxItemSizeInBytes = 1024 * 1024 * 50;

    private NodeRefTypeResolver _nodeRefResolver = new NodeRefTypeResolver();

    public void setProfileNod(String nod) {
        _profileNod = nod;
    }

    public void setProfilePM(String pm) {
        _profilePM = pm;
    }

    public void setProfileNatn(Collection<String> natn) {
        _profileNatn = natn;
    }

    public void setMaxItemSizeInBytes(int maxSize) {
        _maxItemSizeInBytes = maxSize;
    }

    @Override
    public Date getModifiedDate(NodeRef nodeRef) {

        //People
        if (isNodeRefProfile(nodeRef)) {
            return (Date) _nodeService.getProperty(nodeRef, UserProfileModel.PROP_MODIFIED);
        }

        //Things
        return (Date) _nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED); //I was a little worried about this cast but it's in the alfresco source
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getModifiedBy(final NodeRef nodeRef) {
        final String userName = (String) _nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIER);

        String modifiedBy = null;

        if (userName != null) {
            final NodeRef person = _personService.getPerson(userName);

            if (person != null) {
                final String firstName = (String) _nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME);
                final String lastName = (String) _nodeService.getProperty(person, ContentModel.PROP_LASTNAME);

                final StringBuilder fullNameBuilder = new StringBuilder();

                // Strip out the first name if it is "Officer" or the username.
                if (!firstName.equals(userName) && !firstName.equalsIgnoreCase(OFFICER)) {
                    fullNameBuilder.append(firstName);
                    fullNameBuilder.append(' ');
                }

                // String out the last name if it is just the username.
                if (!lastName.equals(userName)) {
                    fullNameBuilder.append(lastName);
                }

                final String fullName = fullNameBuilder.toString().trim();

                // If we have a useful full name, use it, otherwise just use
                // the username alone as the modifiedBy value.
                if (fullName.length() > 0) {
                    modifiedBy = String.format("%s (%s)", userName, fullName);
                } else {
                    modifiedBy = userName;
                }
            }
        }

        return modifiedBy;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NodeRefType getDocumentType(final NodeRef nodeRef) {
        return _nodeRefResolver.getType(_nodeService, nodeRef);
    }

    /**
     * Retrieve a URL to the given nodeRef.  The URL must be a share URL.  To do this,
     * we interrogate the document path and delegate to different private methods depending on the type
     * of the content item as inferred from it's path.
     * @param nodeRef The item to retrieve the URL for, which should both exist and refer to a cm:content 
     * node managed by Share
     * @return String representing a url <i>fragment</i>, starting from the context root (ie. /share) and 
     * finishing at the end of the URL - a properley authenticated user visiting this URL will be able to
     * see the target nodeRef in context
     */
    @Override
    public String getURL(NodeRef nodeRef) {

        String path = _nodeService.getPath(nodeRef).toString();
        if (_logger.isDebugEnabled()) {
            _logger.debug("Getting url to " + nodeRef + " which has path " + path);
        }
        if (path.indexOf("documentLibrary/") != -1) {
            if (path.indexOf(" discussion/") != -1) //We can get away with this because a document called "foo discussion" would have the space escaped to "foo%20discussion"
            {
                return getDocumentCommentURL(nodeRef);
            }
            return getDocLibURL(nodeRef);
        } else if (path.indexOf("wiki/") != -1) {
            return getWikiURL(nodeRef);
        } else if (path.indexOf("discussions/") != -1) {
            return getDiscussionURL(nodeRef);
        } else if (isNodeRefProfile(nodeRef)) {
            return getProfileURL(nodeRef);
        }
        return getUnknownItemURL(nodeRef);
    }

    protected boolean isNodeRefProfile(NodeRef nodeRef) {
        return (_nodeService.getPath(nodeRef).toString().indexOf("people/") != -1);
    }

    private String getSiteName(String path) {
        String fromBeginning = path.substring(path.indexOf(SITE_PART_OF_PATH) + SITE_PART_OF_PATH.length());
        return fromBeginning.substring(0, fromBeginning.indexOf("/"));
    }

    private String getSiteName(NodeRef nodeRef) {
        return getSiteName(_nodeService.getPath(nodeRef).toString());
    }

    private String getProfileURL(NodeRef nodeRef) {
        return "/share/page/user/" + _nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME) + "/profile";
    }

    private String getDocLibURL(NodeRef nodeRef) {
        if (_logger.isDebugEnabled()) {
            _logger.debug("Getting a document library URL");
        }
        return "/share/page/site/" + getSiteName(nodeRef) + "/document-details?nodeRef=" + nodeRef.toString();
    }

    private String getWikiURL(NodeRef nodeRef) {
        if (_logger.isDebugEnabled()) {
            _logger.debug("Getting a wiki URL");
        }
        String pathStr = _nodeService.getPath(nodeRef).toString();
        return "/share/page/site/" + getSiteName(pathStr) + "/wiki-page?title="
                + pathStr.substring(pathStr.lastIndexOf("}") + 1);
    }

    private String getDiscussionURL(NodeRef nodeRef) {
        if (_logger.isDebugEnabled()) {
            _logger.debug("Getting a discussion URL");
        }
        QName type = _nodeService.getType(nodeRef);

        //We can assume that we've been passed the node of a post somewhere within the hierachy of a discussion.
        //But in order to generate a URL for a visible, in-context web page, we need to base our URL on the node
        //of the discussion topic as a whole.  So we navigate up the node hierachy until we reach a node with type
        //"fm:topic", which is the root of the discussion, and use that
        while (!type.equals(ForumModel.TYPE_TOPIC)) {
            nodeRef = _nodeService.getPrimaryParent(nodeRef).getParentRef();
            if (nodeRef == null) {
                throw new GSAProcessingException(
                        "Could not generate a url for " + nodeRef + " which GSA identified as a discussion",
                        new NullPointerException(), 90210);
            }
            type = _nodeService.getType(nodeRef);
        }

        if (_logger.isDebugEnabled()) {
            _logger.debug("Root node of the topic was identified as: " + nodeRef);
        }

        return "/share/page/site/" + getSiteName(nodeRef) + "/discussions-topicview?topicId="
                + _nodeService.getProperty(nodeRef, ContentModel.PROP_NAME) + "&listViewLinkBack=true";
    }

    private String getUnknownItemURL(NodeRef nodeRef) {
        //This isn't implemented yet.  This is intentionally just a hook for a subclass to use if needed
        throw new GSAProcessingException("The type of " + nodeRef.toString() + " could not be determined", null,
                90211);
    }

    /**
     * For document comments, get the grandparent of the comment, which is the document itself, and return a URL to that
     */
    private String getDocumentCommentURL(NodeRef nodeRef) {
        if (_logger.isDebugEnabled()) {
            _logger.debug("Getting a document comment URL");
        }

        NodeRef parent = _nodeService.getPrimaryParent(nodeRef).getParentRef();
        NodeRef grandParent = _nodeService.getPrimaryParent(parent).getParentRef();
        return getDocLibURL(_nodeService.getPrimaryParent(grandParent).getParentRef());
    }

    @Override
    /**
     * Retrieve the size, in charecters, of the content held under the given node ref
     */
    public long getContentSize(NodeRef nodeRef) {
        if (isNodeRefProfile(nodeRef)) {
            return getUnescapedProfileContent(nodeRef).length();
        }
        ContentReader reader = _contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
        return reader.getSize();
    }

    /**
     * If the item has a cm:title property, we return that.  Otherwise, we extract the "filename" part
     * of the item's path, and use that.  Better behavior for discussion posts would be to navigate
     * up the node hierachy to the topic root and use the title of that, with something similar for document
     * comments
     */
    @Override
    public String getTitle(NodeRef nodeRef) {

        //People
        if (isNodeRefProfile(nodeRef)) {
            return _nodeService.getProperty(nodeRef, ContentModel.PROP_FIRSTNAME) + " "
                    + _nodeService.getProperty(nodeRef, ContentModel.PROP_LASTNAME);
        }

        //Things
        String title = (String) _nodeService.getProperty(nodeRef, ContentModel.PROP_TITLE);

        if (title == null || title.trim().length() < 1) //If we don't have a title, use the last bit of the path
        {

            if (_logger.isDebugEnabled()) {
                _logger.debug("No title available for " + nodeRef + " so using the path");
            }

            String lastBitOfPath = _nodeService.getPath(nodeRef).last().getElementString();
            int endOfNamespaceIdx = lastBitOfPath.indexOf('}');
            try {
                return lastBitOfPath.substring(endOfNamespaceIdx + 1);
            } catch (StringIndexOutOfBoundsException e) {
                throw new GSAProcessingException(
                        "An invalid path was found: Namespace expected in " + lastBitOfPath, e, 3681);
            } catch (NullPointerException npe) {
                throw new GSAProcessingException("No path could be found where one was expected", npe, 3682);
            }
        }
        return title;
    }

    /**
     * We have this in a seperate method as we need to retrieve it's length seperately
     * @param nodeRef
     * @return
     */
    private String getUnescapedProfileContent(NodeRef nodeRef) {
        //Start with the bio
        StringBuffer sb = new StringBuffer(200);

        Serializable bio = _nodeService.getProperty(nodeRef, UserProfileModel.PROP_BIOGRAPHY);
        if (bio != null) {
            sb.append(bio).append(" ");
        }

        //Add amas, if present
        Collection<Serializable> amaColl = ((Collection<Serializable>) _nodeService.getProperty(nodeRef,
                UserProfileModel.PROP_ASK_ME_ABOUT));
        if (amaColl != null) {
            Iterator<Serializable> amas = amaColl.iterator();
            while (amas.hasNext()) {
                sb.append(amas.next()).append(" ");
            }
        }

        //Add telephone numbers
        Collection<Serializable> telephoneColl = ((Collection<Serializable>) _nodeService.getProperty(nodeRef,
                UserProfileModel.PROP_TELEPHONE_NUMBERS));
        if (telephoneColl != null) {
            Iterator<Serializable> telephones = telephoneColl.iterator();

            //Telephone numbers are a formatted comma seperated String - search engines might not handle the commas as we would like so let's replace with a space
            while (telephones.hasNext()) {
                sb.append(telephones.next().toString().replaceAll(",", " ")).append(" ");
            }
        }

        //Construct, log and return output String
        String output = sb.toString();
        if (_logger.isDebugEnabled()) {
            _logger.debug("NodeRef " + nodeRef + " contained the following profile data: " + output);
        }
        return output;
    }

    /**
     * This method isn't optimised for very large content items, and will break entirely with anything > 4Gb, but that's
     * more than fine given our use case
     */
    @Override
    public String getContent(NodeRef nodeRef) {

        //People
        if (isNodeRefProfile(nodeRef)) {
            return escapeContentString(getUnescapedProfileContent(nodeRef));
        }

        //Things
        ContentReader reader = _contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);

        int dataSize = (int) getContentSize(nodeRef);
        if (dataSize > _maxItemSizeInBytes) {
            _logger.info("Truncating " + nodeRef + " from " + reader.getSize() + " to " + _maxItemSizeInBytes
                    + " bytes");
            dataSize = _maxItemSizeInBytes;
        }

        byte[] data = new byte[dataSize]; //Seriously, you shouldn't be trying to get 4Gb of data using this method anyway
        BufferedInputStream bis = null;
        try {
            bis = new BufferedInputStream(reader.getContentInputStream());
            bis.read(data, 0, dataSize);
        } catch (Exception e) {
            throw new GSAProcessingException("Error reading content data for " + nodeRef, e, 100000);
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    _logger.warn("Could not close byte array input stream - ignoring");
                }
            }
        }
        _logger.debug(data.length + " bytes of content data returned for " + nodeRef);
        return escapeContentString(data);
    }

    public String getMimeType(NodeRef nodeRef) {

        //People - always text/plain as we are assembling the profile information from String fields
        if (isNodeRefProfile(nodeRef)) {
            return "text/plain";
        }

        //Things
        return _contentService.getReader(nodeRef, ContentModel.PROP_CONTENT).getMimetype();
    }

    /**
     * Pretty self-explanatory - some fiddling about to deal with the fact that properties may be null;
     */
    @Override
    public SecurityLabel getSecurityLabel(NodeRef nodeRef) {

        //People don't really have a security label, so just return a constant, injected, value
        if (isNodeRefProfile(nodeRef)) {
            SecurityLabel label = new SecurityLabel();
            label.setNOD(_profileNod);
            label.setProtectiveMarking(_profilePM);
            Iterator<String> natns = _profileNatn.iterator();
            while (natns.hasNext()) {
                label.addNationalityCaveat(natns.next());
            }
            return label;
        }

        //Things
        SecurityLabel label = new SecurityLabel();

        //These setters should just accept 'null' as inputs so need no checking
        label.setNOD((String) (_nodeService.getProperty(nodeRef, NOD_QN)));
        label.setProtectiveMarking((String) (_nodeService.getProperty(nodeRef, PM_QN)));
        label.setFreeformCaveats((String) (_nodeService.getProperty(nodeRef, FREEFORM_QN)));

        Serializable openMarkingsList = _nodeService.getProperty(nodeRef, OG_QN);
        if (openMarkingsList != null) {
            Iterator<String> openMarkings = ((List<String>) openMarkingsList).iterator();
            while (openMarkings.hasNext()) {
                SecurityMarking marking = new SecurityMarking();
                marking.setName(openMarkings.next());
                marking.setType(SecurityMarkingType.OPEN);
                label.addMarking(marking);
            }
        }

        Serializable closedMarkingsList = _nodeService.getProperty(nodeRef, CG_QN);
        if (closedMarkingsList != null) {
            Iterator<String> closedMarkings = ((List<String>) (closedMarkingsList)).iterator();
            while (closedMarkings.hasNext()) {
                SecurityMarking marking = new SecurityMarking();
                marking.setName(closedMarkings.next());
                marking.setType(SecurityMarkingType.CLOSED);
                label.addMarking(marking);
            }
        }

        Serializable organisationsList = _nodeService.getProperty(nodeRef, ORG_QN);
        if (organisationsList != null) {
            Iterator<String> organisations = ((List<String>) (organisationsList)).iterator();
            while (organisations.hasNext()) {
                SecurityMarking marking = new SecurityMarking();
                marking.setName(organisations.next());
                marking.setType(SecurityMarkingType.ORGANISATION);
                label.addMarking(marking);
            }
        }

        String[] natns = getNationalCaveats(nodeRef);
        if (natns != null) {
            for (int i = 0; i < natns.length; i++) {
                label.addNationalityCaveat(natns[i]);
            }
        }

        return label;
    }

    protected String[] getNationalCaveats(NodeRef nodeRef) {
        String nationalityCaveats = getNationalCaveatsString(nodeRef);
        if (nationalityCaveats != null && nationalityCaveats.trim().length() > 1) {
            return nationalityCaveats.replaceAll("EYES ONLY", "").trim().split("/");
        }
        return null;
    }

    protected String getNationalCaveatsString(NodeRef nodeRef) {
        return (String) (_nodeService.getProperty(nodeRef, NATN_QN));
    }

    /**
     * This method will produce an incorrect encoding when called with a String created from a non-unicode binary file.
     * If you're not processing character data, use the byte array version instead
     */
    protected String escapeContentString(String content) {
        return escapeContentString(content.getBytes());
    }

    protected String escapeContentString(byte[] data) {
        return Base64.encodeBytes(data);
    }

    @Override
    public void setNodeService(NodeService nodeService) {
        _nodeService = nodeService;
    }

    @Override
    public void setContentService(ContentService contentService) {
        _contentService = contentService;
    }

    @Override
    public void setPersonService(PersonService personService) {
        _personService = personService;
    }
}