org.alfresco.repo.node.MLPropertyInterceptor.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.node.MLPropertyInterceptor.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.node;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.alfresco.model.ContentModel;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.ml.MultilingualContentService;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;

/**
 * Interceptor to filter out multilingual text properties from getter methods and
 * transform to multilingual text for setter methods.
 * <p>
 * This interceptor ensures that all multilingual (ML) text is transformed to the
 * locale chosen for the request
 * for getters and transformed to the default locale type for setters.
 * <p>
 * Where {@link org.alfresco.service.cmr.repository.MLText ML text} has been passed in, this
 * will be allowed to pass.
 * 
 * @see org.alfresco.service.cmr.repository.NodeService#getProperty(NodeRef, QName)
 * @see org.alfresco.service.cmr.repository.NodeService#getProperties(NodeRef)
 * @see org.alfresco.service.cmr.repository.NodeService#setProperty(NodeRef, QName, Serializable)
 * @see org.alfresco.service.cmr.repository.NodeService#setProperties(NodeRef, Map)
 * 
 * @author Derek Hulley
 * @author Philippe Dubois
 */
public class MLPropertyInterceptor implements MethodInterceptor {
    private static Log logger = LogFactory.getLog(MLPropertyInterceptor.class);

    private static ThreadLocal<Boolean> mlAware = new ThreadLocal<Boolean>();

    /** Direct access to the NodeService */
    private NodeService nodeService;

    /** Direct access to the MultilingualContentService */
    private MultilingualContentService multilingualContentService;

    /** Used to access property definitions */
    private DictionaryService dictionaryService;

    /**
     * Change the filtering behaviour of this interceptor on the curren thread.
     * Use this to switch off the filtering and just pass out properties as
     * handed out of the node service.
     * 
     * @param mlAwareVal <tt>true</tt> if the current thread is able to handle
     *      {@link MLText d:mltext} property types, otherwise <tt>false</tt>.
     * @return
     *      <tt>true</tt> if the current transaction is ML aware
     */
    static public boolean setMLAware(boolean mlAwareVal) {
        boolean wasMLAware = isMLAware();
        mlAware.set(Boolean.valueOf(mlAwareVal));
        return wasMLAware;
    }

    /**
     * @return Returns <tt>true</tt> if the current thread has marked itself
     *      as being able to handle {@link MLText d:mltext} types properly.
     */
    static public boolean isMLAware() {
        if (mlAware.get() == null) {
            return false;
        } else {
            return mlAware.get();
        }
    }

    public void setNodeService(NodeService bean) {
        this.nodeService = bean;
    }

    public void setMultilingualContentService(MultilingualContentService multilingualContentService) {
        this.multilingualContentService = multilingualContentService;
    }

    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    @SuppressWarnings("unchecked")
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        if (logger.isDebugEnabled()) {
            logger.debug("Intercepting method " + invocation.getMethod().getName() + " using content filter "
                    + I18NUtil.getContentLocale());
        }

        // If isMLAware then no treatment is done, just return
        if (isMLAware()) {
            // Don't interfere
            return invocation.proceed();
        }

        Locale contentLangLocale = I18NUtil.getContentLocaleLang();

        Object ret = null;

        final String methodName = invocation.getMethod().getName();
        final Object[] args = invocation.getArguments();

        if (methodName.equals("getProperty")) {
            NodeRef nodeRef = (NodeRef) args[0];
            QName propertyQName = (QName) args[1];

            // Get the pivot translation, if appropriate
            NodeRef pivotNodeRef = getPivotNodeRef(nodeRef);

            // What locale must be used for filtering - ALF-3756 fix, ignore the country and variant
            Serializable value = (Serializable) invocation.proceed();
            ret = convertOutboundProperty(nodeRef, pivotNodeRef, propertyQName, value);
        } else if (methodName.equals("getProperties")) {
            NodeRef nodeRef = (NodeRef) args[0];

            // Get the pivot translation, if appropriate
            NodeRef pivotNodeRef = getPivotNodeRef(nodeRef);

            Map<QName, Serializable> properties = (Map<QName, Serializable>) invocation.proceed();
            Map<QName, Serializable> convertedProperties = new HashMap<QName, Serializable>(properties.size() * 2);
            // Check each return value type
            for (Map.Entry<QName, Serializable> entry : properties.entrySet()) {
                QName propertyQName = entry.getKey();
                Serializable value = entry.getValue();
                Serializable convertedValue = convertOutboundProperty(nodeRef, pivotNodeRef, propertyQName, value);
                // Add it to the return map
                convertedProperties.put(propertyQName, convertedValue);
            }
            ret = convertedProperties;
            // Done
            if (logger.isDebugEnabled()) {
                logger.debug("Converted getProperties return value: \n" + "   initial:   " + properties + "\n"
                        + "   converted: " + convertedProperties);
            }
        } else if (methodName.equals("setProperties")) {
            NodeRef nodeRef = (NodeRef) args[0];
            Map<QName, Serializable> newProperties = (Map<QName, Serializable>) args[1];

            // Get the pivot translation, if appropriate
            NodeRef pivotNodeRef = getPivotNodeRef(nodeRef);

            // Get the current properties for the node
            Map<QName, Serializable> currentProperties = nodeService.getProperties(nodeRef);
            // Convert all properties
            Map<QName, Serializable> convertedProperties = convertInboundProperties(currentProperties,
                    newProperties, contentLangLocale, nodeRef, pivotNodeRef);
            // Now complete the call by passing the converted properties
            nodeService.setProperties(nodeRef, convertedProperties);
            // Done
        } else if (methodName.equals("addProperties")) {
            NodeRef nodeRef = (NodeRef) args[0];
            Map<QName, Serializable> newProperties = (Map<QName, Serializable>) args[1];

            // Get the pivot translation, if appropriate
            NodeRef pivotNodeRef = getPivotNodeRef(nodeRef);

            // Get the current properties for the node
            Map<QName, Serializable> currentProperties = nodeService.getProperties(nodeRef);
            // Convert all properties
            Map<QName, Serializable> convertedProperties = convertInboundProperties(currentProperties,
                    newProperties, contentLangLocale, nodeRef, pivotNodeRef);
            // Now complete the call by passing the converted properties
            nodeService.addProperties(nodeRef, convertedProperties);
            // Done
        } else if (methodName.equals("setProperty")) {
            NodeRef nodeRef = (NodeRef) args[0];
            QName propertyQName = (QName) args[1];
            Serializable inboundValue = (Serializable) args[2];

            // Get the pivot translation, if appropriate
            NodeRef pivotNodeRef = getPivotNodeRef(nodeRef);

            // Convert the property
            inboundValue = convertInboundProperty(contentLangLocale, nodeRef, pivotNodeRef, propertyQName,
                    inboundValue, null);

            // Pass this through to the node service
            nodeService.setProperty(nodeRef, propertyQName, inboundValue);
            // Done
        } else if (methodName.equals("createNode") && args.length > 4) {
            NodeRef parentNodeRef = (NodeRef) args[0];
            QName assocTypeQName = (QName) args[1];
            QName assocQName = (QName) args[2];
            QName nodeTypeQName = (QName) args[3];
            Map<QName, Serializable> newProperties = (Map<QName, Serializable>) args[4];
            if (newProperties == null) {
                newProperties = Collections.emptyMap();
            }
            NodeRef nodeRef = null; // Not created yet

            // No pivot
            NodeRef pivotNodeRef = null;

            // Convert all properties
            Map<QName, Serializable> convertedProperties = convertInboundProperties(null, newProperties,
                    contentLangLocale, nodeRef, pivotNodeRef);
            // Now complete the call by passing the converted properties
            ret = nodeService.createNode(parentNodeRef, assocTypeQName, assocQName, nodeTypeQName,
                    convertedProperties);
            // Done
        } else if (methodName.equals("addAspect") && args[2] != null) {
            NodeRef nodeRef = (NodeRef) args[0];
            QName aspectTypeQName = (QName) args[1];

            // Get the pivot translation, if appropriate
            NodeRef pivotNodeRef = getPivotNodeRef(nodeRef);

            Map<QName, Serializable> newProperties = (Map<QName, Serializable>) args[2];
            // Get the current properties for the node
            Map<QName, Serializable> currentProperties = nodeService.getProperties(nodeRef);
            // Convert all properties
            Map<QName, Serializable> convertedProperties = convertInboundProperties(currentProperties,
                    newProperties, contentLangLocale, nodeRef, pivotNodeRef);
            // Now complete the call by passing the converted properties
            nodeService.addAspect(nodeRef, aspectTypeQName, convertedProperties);
            // Done
        } else {
            ret = invocation.proceed();
        }
        // done
        return ret;
    }

    /**
     * @param nodeRef
     *      a potential empty translation
     * @return
     *      the pivot translation node or <tt>null</tt>
     */
    private NodeRef getPivotNodeRef(NodeRef nodeRef) {
        if (nodeRef == null) {
            throw new IllegalArgumentException(
                    "NodeRef may not be null for calls to NodeService.  Check client code.");
        }
        if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION)) {
            return multilingualContentService.getPivotTranslation(nodeRef);
        } else {
            return null;
        }
    }

    /**
     * Ensure that content is spoofed for empty translations.
     */
    private Serializable convertOutboundProperty(NodeRef nodeRef, NodeRef pivotNodeRef, QName propertyQName,
            Serializable outboundValue) {
        Serializable ret = null;
        if (outboundValue == null) {
            ret = null;
        }
        if (outboundValue instanceof MLText) {
            // It is MLText
            MLText mlText = (MLText) outboundValue;
            ret = getClosestValue(mlText);
        } else if (isCollectionOfMLText(outboundValue)) {
            Collection<?> col = (Collection<?>) outboundValue;
            ArrayList<String> answer = new ArrayList<String>(col.size());
            Locale closestLocale = getClosestLocale(col);
            for (Object o : col) {
                MLText mlText = (MLText) o;
                String value = mlText.get(closestLocale);
                if (value != null) {
                    answer.add(value);
                }
            }
            ret = answer;
        } else if (pivotNodeRef != null) // It is an empty translation
        {
            if (propertyQName.equals(ContentModel.PROP_MODIFIED)) {
                // An empty translation's modified date must be the later of its own
                // modified date and the pivot translation's modified date
                Date emptyLastModified = (Date) outboundValue;
                Date pivotLastModified = (Date) nodeService.getProperty(pivotNodeRef, ContentModel.PROP_MODIFIED);
                if (emptyLastModified.compareTo(pivotLastModified) < 0) {
                    ret = pivotLastModified;
                } else {
                    ret = emptyLastModified;
                }
            } else if (propertyQName.equals(ContentModel.PROP_CONTENT)) {
                // An empty translation's cm:content must track the cm:content of the
                // pivot translation.
                ret = nodeService.getProperty(pivotNodeRef, ContentModel.PROP_CONTENT);
            } else {
                ret = outboundValue;
            }
        } else {
            ret = outboundValue;
        }
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Converted outbound property: \n" + "   NodeRef:        " + nodeRef + "\n"
                    + "   Property:       " + propertyQName + "\n" + "   Before:         " + outboundValue + "\n"
                    + "   After:          " + ret);
        }
        return ret;
    }

    private Serializable getClosestValue(MLText mlText) {
        Set<Locale> locales = mlText.getLocales();
        Locale contentLocale = I18NUtil.getContentLocale();
        Locale locale = I18NUtil.getNearestLocale(contentLocale, locales);
        if (locale != null) {
            return mlText.getValue(locale);
        }

        // If the content locale is too specific, try relaxing it to just language
        Locale contentLocaleLang = I18NUtil.getContentLocaleLang();
        // We do not expect contentLocaleLang to be null
        if (contentLocaleLang != null) {
            locale = I18NUtil.getNearestLocale(contentLocaleLang, locales);
            if (locale != null) {
                return mlText.getValue(locale);
            }
        } else {
            logger.warn("contentLocaleLang is null in getClosestValue. This is not expected.");
        }

        // Just return the default translation
        return mlText.getDefaultValue();
    }

    public Locale getClosestLocale(Collection<?> collection) {
        if (collection.size() == 0) {
            return null;
        }
        // Use the available keys as options
        HashSet<Locale> locales = new HashSet<Locale>();
        for (Object o : collection) {
            MLText mlText = (MLText) o;
            locales.addAll(mlText.keySet());
        }
        // Try the content locale
        Locale locale = I18NUtil.getContentLocale();
        Locale match = I18NUtil.getNearestLocale(locale, locales);
        if (match == null) {
            // Try just the content locale language
            locale = I18NUtil.getContentLocaleLang();
            match = I18NUtil.getNearestLocale(locale, locales);
            if (match == null) {
                // No close matches for the locale - go for the default locale
                locale = I18NUtil.getLocale();
                match = I18NUtil.getNearestLocale(locale, locales);
                if (match == null) {
                    // just get any locale
                    match = I18NUtil.getNearestLocale(null, locales);
                }
            }
        }
        return match;
    }

    /**
     * @param outboundValue Serializable
     * @return boolean
     */
    private boolean isCollectionOfMLText(Serializable outboundValue) {
        if (outboundValue instanceof Collection<?>) {
            for (Object o : (Collection<?>) outboundValue) {
                if (!(o instanceof MLText)) {
                    return false;
                }
            }
            return true;
        } else {
            return false;
        }
    }

    private Map<QName, Serializable> convertInboundProperties(Map<QName, Serializable> currentProperties,
            Map<QName, Serializable> newProperties, Locale contentLocale, NodeRef nodeRef, NodeRef pivotNodeRef) {
        Map<QName, Serializable> convertedProperties = new HashMap<QName, Serializable>(newProperties.size() * 2);
        for (Map.Entry<QName, Serializable> entry : newProperties.entrySet()) {
            QName propertyQName = entry.getKey();
            Serializable inboundValue = entry.getValue();
            // Get the current property value
            Serializable currentValue = currentProperties == null ? null : currentProperties.get(propertyQName);
            // Convert the inbound property value
            inboundValue = convertInboundProperty(contentLocale, nodeRef, pivotNodeRef, propertyQName, inboundValue,
                    currentValue);
            // Put the value into the map
            convertedProperties.put(propertyQName, inboundValue);
        }
        return convertedProperties;
    }

    /**
     * 
     * @param inboundValue      The value that must be set
     * @param currentValue      The current value of the property or <tt>null</tt> if not known
     * @return                  Returns a potentially converted property that conforms to the model
     */
    private Serializable convertInboundProperty(Locale contentLocale, NodeRef nodeRef, NodeRef pivotNodeRef,
            QName propertyQName, Serializable inboundValue, Serializable currentValue) {
        Serializable ret = null;
        PropertyDefinition propertyDef = this.dictionaryService.getProperty(propertyQName);
        //if no type definition associated to the name then just proceed
        if (propertyDef == null) {
            ret = inboundValue;
        } else if (propertyDef.getDataType().getName().equals(DataTypeDefinition.MLTEXT)) {
            // Don't mess with multivalued properties or instances already of type MLText
            if (inboundValue instanceof MLText) {
                ret = inboundValue;
            } else if (propertyDef.isMultiValued()) {
                // leave collectios of ML text alone
                if (isCollectionOfMLText(inboundValue)) {
                    ret = inboundValue;
                } else {
                    // Anything else we assume is localised
                    if (currentValue == null && nodeRef != null) {
                        currentValue = nodeService.getProperty(nodeRef, propertyQName);
                    }
                    ArrayList<MLText> returnMLList = new ArrayList<MLText>();
                    if (currentValue != null) {
                        Collection<MLText> currentCollection = DefaultTypeConverter.INSTANCE
                                .getCollection(MLText.class, currentValue);
                        returnMLList.addAll(currentCollection);
                    }
                    Collection<String> inboundCollection = DefaultTypeConverter.INSTANCE.getCollection(String.class,
                            inboundValue);
                    int count = 0;
                    for (String current : inboundCollection) {
                        MLText newMLValue;
                        if (count < returnMLList.size()) {
                            MLText currentMLValue = returnMLList.get(count);
                            newMLValue = new MLText();
                            if (currentMLValue != null) {
                                newMLValue.putAll(currentMLValue);
                            }
                        } else {
                            newMLValue = new MLText();
                        }
                        replaceTextForLanguage(contentLocale, current, newMLValue);
                        if (count < returnMLList.size()) {
                            returnMLList.set(count, newMLValue);
                        } else {
                            returnMLList.add(newMLValue);
                        }
                        count++;
                    }
                    // remove locale settings for anything after
                    for (int i = count; i < returnMLList.size(); i++) {
                        MLText currentMLValue = returnMLList.get(i);
                        MLText newMLValue = new MLText();
                        if (currentMLValue != null) {
                            newMLValue.putAll(currentMLValue);
                        }
                        newMLValue.remove(contentLocale);
                        returnMLList.set(i, newMLValue);
                    }
                    // tidy up empty locales
                    ArrayList<MLText> tidy = new ArrayList<MLText>();
                    for (MLText mlText : returnMLList) {
                        if (mlText.keySet().size() > 0) {
                            tidy.add(mlText);
                        }
                    }
                    ret = tidy;
                }
            } else {
                // This is a multilingual single-valued property
                // Get the current value from the node service, if not provided
                if (currentValue == null && nodeRef != null) {
                    currentValue = nodeService.getProperty(nodeRef, propertyQName);
                }
                MLText returnMLValue = new MLText();
                if (currentValue != null) {
                    MLText currentMLValue = DefaultTypeConverter.INSTANCE.convert(MLText.class, currentValue);
                    returnMLValue.putAll(currentMLValue);
                }
                // Force the inbound value to be a String (it isn't MLText)
                String inboundValueStr = DefaultTypeConverter.INSTANCE.convert(String.class, inboundValue);
                // Update the text for the appropriate language.
                replaceTextForLanguage(contentLocale, inboundValueStr, returnMLValue);
                // Done
                ret = returnMLValue;
            }
        } else if (pivotNodeRef != null && propertyQName.equals(ContentModel.PROP_CONTENT)) {
            // It is an empty translation.  The content must not change if it matches
            // the content of the pivot translation
            ContentData pivotContentData = (ContentData) nodeService.getProperty(pivotNodeRef,
                    ContentModel.PROP_CONTENT);
            ContentData emptyContentData = (ContentData) inboundValue;
            String pivotContentUrl = pivotContentData == null ? null : pivotContentData.getContentUrl();
            String emptyContentUrl = emptyContentData == null ? null : emptyContentData.getContentUrl();
            if (EqualsHelper.nullSafeEquals(pivotContentUrl, emptyContentUrl)) {
                // They are a match.  So the empty translation must be reset to it's original value
                ret = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
            } else {
                ret = inboundValue;
            }
        } else {
            ret = inboundValue;
        }
        // Done
        if (logger.isDebugEnabled() && ret != inboundValue) {
            logger.debug("Converted inbound property: \n" + "   NodeRef:    " + nodeRef + "\n" + "   Property:   "
                    + propertyQName + "\n" + "   Before:     " + inboundValue + "\n" + "   After:      " + ret);
        }
        return ret;
    }

    /**
     * Replace any text in mlText having the same language (but any variant) as contentLocale
     * with updatedText keyed by the language of contentLocale. This ensures that the mlText
     * will have no more than one entry for the particular language.
     * 
     * @param contentLocale Locale
     * @param updatedText String
     * @param mlText MLText
     */
    private void replaceTextForLanguage(Locale contentLocale, String updatedText, MLText mlText) {
        String language = contentLocale.getLanguage();
        // Remove all text entries having the same language as the chosen contentLocale
        // (e.g. if contentLocale is en_GB, then remove text for en, en_GB, en_US etc.
        Iterator<Locale> locales = mlText.getLocales().iterator();
        while (locales.hasNext()) {
            Locale locale = locales.next();
            if (locale.getLanguage().equals(language)) {
                locales.remove();
            }
        }

        // Add the new value for the specific language
        mlText.addValue(new Locale(language), updatedText);
    }
}