Java tutorial
/* * #%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); } }