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.domain.node; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.domain.locale.LocaleDAO; import org.alfresco.repo.domain.qname.QNameDAO; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryException; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.MLText; import org.alfresco.service.cmr.repository.datatype.TypeConversionException; import org.alfresco.service.namespace.QName; import org.alfresco.util.EqualsHelper; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class provides services for translating exploded properties * (as persisted in <b>alf_node_properties</b>) in the public form, which is a * <tt>Map</tt> of values keyed by their <tt>QName</tt>. * * @author Derek Hulley * @since 3.4 */ public class NodePropertyHelper { private static final Log logger = LogFactory.getLog(NodePropertyHelper.class); private final DictionaryService dictionaryService; private final QNameDAO qnameDAO; private final LocaleDAO localeDAO; private final ContentDataDAO contentDataDAO; /** * Construct the helper with the appropriate DAOs and services */ public NodePropertyHelper(DictionaryService dictionaryService, QNameDAO qnameDAO, LocaleDAO localeDAO, ContentDataDAO contentDataDAO) { this.dictionaryService = dictionaryService; this.qnameDAO = qnameDAO; this.localeDAO = localeDAO; this.contentDataDAO = contentDataDAO; } public Map<NodePropertyKey, NodePropertyValue> convertToPersistentProperties(Map<QName, Serializable> in) { // Get the locale ID (the default will be overridden where necessary) Long propertylocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst(); Map<NodePropertyKey, NodePropertyValue> propertyMap = new HashMap<NodePropertyKey, NodePropertyValue>( in.size() + 5); for (Map.Entry<QName, Serializable> entry : in.entrySet()) { Serializable value = entry.getValue(); // Get the qname ID QName propertyQName = entry.getKey(); Long propertyQNameId = qnameDAO.getOrCreateQName(propertyQName).getFirst(); // Get the property definition, if available PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); // Add it to the map addValueToPersistedProperties(propertyMap, propertyDef, NodePropertyHelper.IDX_NO_COLLECTION, propertyQNameId, propertylocaleId, value); } // Done return propertyMap; } /** * The collection index used to indicate that the value is not part of a collection. All values from zero up are * used for real collection indexes. */ private static final int IDX_NO_COLLECTION = -1; /** * A method that adds properties to the given map. It copes with collections. * * @param propertyDef the property definition (<tt>null</tt> is allowed) * @param collectionIndex the index of the property in the collection or <tt>-1</tt> if we are not yet processing a * collection */ private void addValueToPersistedProperties(Map<NodePropertyKey, NodePropertyValue> propertyMap, PropertyDefinition propertyDef, int collectionIndex, Long propertyQNameId, Long propertyLocaleId, Serializable value) { if (value == null) { // The property is null. Null is null and cannot be massaged any other way. NodePropertyValue npValue = makeNodePropertyValue(propertyDef, null); NodePropertyKey npKey = new NodePropertyKey(); npKey.setListIndex(collectionIndex); npKey.setQnameId(propertyQNameId); npKey.setLocaleId(propertyLocaleId); // Add it to the map propertyMap.put(npKey, npValue); // Done return; } // Get or spoof the property datatype QName propertyTypeQName; if (propertyDef == null) // property not recognised { // allow it for now - persisting excess properties can be useful sometimes propertyTypeQName = DataTypeDefinition.ANY; } else { propertyTypeQName = propertyDef.getDataType().getName(); } // A property may appear to be multi-valued if the model definition is loose and // an unexploded collection is passed in. Otherwise, use the model-defined behaviour // strictly. boolean isMultiValued; if (propertyTypeQName.equals(DataTypeDefinition.ANY)) { // It is multi-valued if required (we are not in a collection and the property is a new collection) isMultiValued = (value != null) && (value instanceof Collection<?>) && (collectionIndex == IDX_NO_COLLECTION); } else { isMultiValued = propertyDef.isMultiValued(); } // Handle different scenarios. // - Do we need to explode a collection? // - Does the property allow collections? if (collectionIndex == IDX_NO_COLLECTION && isMultiValued && !(value instanceof Collection<?>)) { // We are not (yet) processing a collection but the property should be part of a collection addValueToPersistedProperties(propertyMap, propertyDef, 0, propertyQNameId, propertyLocaleId, value); } else if (collectionIndex == IDX_NO_COLLECTION && value instanceof Collection<?>) { // We are not (yet) processing a collection and the property is a collection i.e. needs exploding // Check that multi-valued properties are supported if the property is a collection if (!isMultiValued) { throw new DictionaryException("A single-valued property of this type may not be a collection: \n" + " Property: " + propertyDef + "\n" + " Type: " + propertyTypeQName + "\n" + " Value: " + value); } // We have an allowable collection. @SuppressWarnings("unchecked") Collection<Object> collectionValues = (Collection<Object>) value; // Persist empty collections directly. This is handled by the NodePropertyValue. if (collectionValues.size() == 0) { NodePropertyValue npValue = makeNodePropertyValue(null, (Serializable) collectionValues); NodePropertyKey npKey = new NodePropertyKey(); npKey.setListIndex(NodePropertyHelper.IDX_NO_COLLECTION); npKey.setQnameId(propertyQNameId); npKey.setLocaleId(propertyLocaleId); // Add it to the map propertyMap.put(npKey, npValue); } // Break it up and recurse to persist the values. collectionIndex = -1; for (Object collectionValueObj : collectionValues) { collectionIndex++; if (collectionValueObj != null && !(collectionValueObj instanceof Serializable)) { throw new IllegalArgumentException("Node properties must be fully serializable, " + "including values contained in collections. \n" + " Property: " + propertyDef + "\n" + " Index: " + collectionIndex + "\n" + " Value: " + collectionValueObj); } Serializable collectionValue = (Serializable) collectionValueObj; try { addValueToPersistedProperties(propertyMap, propertyDef, collectionIndex, propertyQNameId, propertyLocaleId, collectionValue); } catch (Throwable e) { throw new AlfrescoRuntimeException( "Failed to persist collection entry: \n" + " Property: " + propertyDef + "\n" + " Index: " + collectionIndex + "\n" + " Value: " + collectionValue, e); } } } else { // We are either processing collection elements OR the property is not a collection // Collections of collections are only supported by type d:any if (value instanceof Collection<?> && !propertyTypeQName.equals(DataTypeDefinition.ANY)) { throw new DictionaryException( "Collections of collections (Serializable) are only supported by type 'd:any': \n" + " Property: " + propertyDef + "\n" + " Type: " + propertyTypeQName + "\n" + " Value: " + value); } // Handle MLText if (value instanceof MLText) { // This needs to be split up into individual strings MLText mlTextValue = (MLText) value; for (Map.Entry<Locale, String> mlTextEntry : mlTextValue.entrySet()) { Locale mlTextLocale = mlTextEntry.getKey(); String mlTextStr = mlTextEntry.getValue(); // Get the Locale ID for the text Long mlTextLocaleId = localeDAO.getOrCreateLocalePair(mlTextLocale).getFirst(); // This is persisted against the current locale, but as a d:text instance // This is persisted against the current locale, but as a d:text instance NodePropertyValue npValue = new NodePropertyValue(DataTypeDefinition.TEXT, mlTextStr); NodePropertyKey npKey = new NodePropertyKey(); npKey.setListIndex(collectionIndex); npKey.setQnameId(propertyQNameId); npKey.setLocaleId(mlTextLocaleId); // Add it to the map propertyMap.put(npKey, npValue); } } else { NodePropertyValue npValue = makeNodePropertyValue(propertyDef, value); NodePropertyKey npKey = new NodePropertyKey(); npKey.setListIndex(collectionIndex); npKey.setQnameId(propertyQNameId); npKey.setLocaleId(propertyLocaleId); // Add it to the map propertyMap.put(npKey, npValue); } } } /** * Helper method to convert the <code>Serializable</code> value into a full, persistable {@link NodePropertyValue}. * <p> * Where the property definition is null, the value will take on the {@link DataTypeDefinition#ANY generic ANY} * value. * <p> * Collections are NOT supported. These must be split up by the calling code before calling this method. Map * instances are supported as plain serializable instances. * * @param propertyDef the property dictionary definition, may be null * @param value the value, which will be converted according to the definition - may be null * @return Returns the persistable property value */ public NodePropertyValue makeNodePropertyValue(PropertyDefinition propertyDef, Serializable value) { // get property attributes final QName propertyTypeQName; if (propertyDef == null) // property not recognised { // allow it for now - persisting excess properties can be useful sometimes propertyTypeQName = DataTypeDefinition.ANY; } else { propertyTypeQName = propertyDef.getDataType().getName(); } try { NodePropertyValue propertyValue = null; propertyValue = new NodePropertyValue(propertyTypeQName, value); // done return propertyValue; } catch (TypeConversionException e) { throw new TypeConversionException( "The property value is not compatible with the type defined for the property: \n" + " property: " + (propertyDef == null ? "unknown" : propertyDef) + "\n" + " value: " + value + "\n" + " value type: " + value.getClass(), e); } } public Serializable getPublicProperty(Map<NodePropertyKey, NodePropertyValue> propertyValues, QName propertyQName) { // Get the qname ID Pair<Long, QName> qnamePair = qnameDAO.getQName(propertyQName); if (qnamePair == null) { // There is no persisted property with that QName, so we can't match anything return null; } Long qnameId = qnamePair.getFirst(); // Now loop over the properties and extract those with the given qname ID SortedMap<NodePropertyKey, NodePropertyValue> scratch = new TreeMap<NodePropertyKey, NodePropertyValue>(); for (Map.Entry<NodePropertyKey, NodePropertyValue> entry : propertyValues.entrySet()) { NodePropertyKey propertyKey = entry.getKey(); if (propertyKey.getQnameId().equals(qnameId)) { scratch.put(propertyKey, entry.getValue()); } } // If we found anything, then collapse the properties to a Serializable if (scratch.size() > 0) { PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); Serializable collapsedValue = collapsePropertiesWithSameQName(propertyDef, scratch); return collapsedValue; } else { return null; } } public Map<QName, Serializable> convertToPublicProperties( Map<NodePropertyKey, NodePropertyValue> propertyValues) { Map<QName, Serializable> propertyMap = new HashMap<QName, Serializable>(propertyValues.size(), 1.0F); // Shortcut if (propertyValues.size() == 0) { return propertyMap; } // We need to process the properties in order SortedMap<NodePropertyKey, NodePropertyValue> sortedPropertyValues = new TreeMap<NodePropertyKey, NodePropertyValue>( propertyValues); // A working map. Ordering is important. SortedMap<NodePropertyKey, NodePropertyValue> scratch = new TreeMap<NodePropertyKey, NodePropertyValue>(); // Iterate (sorted) over the map entries and extract values with the same qname Long currentQNameId = Long.MIN_VALUE; Iterator<Map.Entry<NodePropertyKey, NodePropertyValue>> iterator = sortedPropertyValues.entrySet() .iterator(); while (true) { Long nextQNameId = null; NodePropertyKey nextPropertyKey = null; NodePropertyValue nextPropertyValue = null; // Record the next entry's values if (iterator.hasNext()) { Map.Entry<NodePropertyKey, NodePropertyValue> entry = iterator.next(); nextPropertyKey = entry.getKey(); nextPropertyValue = entry.getValue(); nextQNameId = nextPropertyKey.getQnameId(); } // If the QName is going to change, and we have some entries to process, then process them. if (scratch.size() > 0 && (nextQNameId == null || !nextQNameId.equals(currentQNameId))) { QName currentQName = qnameDAO.getQName(currentQNameId).getSecond(); PropertyDefinition currentPropertyDef = dictionaryService.getProperty(currentQName); // We have added something to the scratch properties but the qname has just changed Serializable collapsedValue = null; // We can shortcut if there is only one value if (scratch.size() == 1) { // There is no need to collapse list indexes collapsedValue = collapsePropertiesWithSameQNameAndListIndex(currentPropertyDef, scratch); } else { // There is more than one value so the list indexes need to be collapsed collapsedValue = collapsePropertiesWithSameQName(currentPropertyDef, scratch); } boolean forceCollection = false; // If the property is multi-valued then the output property must be a collection if (currentPropertyDef != null && currentPropertyDef.isMultiValued()) { forceCollection = true; } else if (scratch.size() == 1 && scratch.firstKey().getListIndex().intValue() > -1) { // This is to handle cases of collections where the property is d:any but not // declared as multiple. forceCollection = true; } if (forceCollection && collapsedValue != null && !(collapsedValue instanceof Collection<?>)) { // Can't use Collections.singletonList: ETHREEOH-1172 ArrayList<Serializable> collection = new ArrayList<Serializable>(1); collection.add(collapsedValue); collapsedValue = collection; } // Store the value propertyMap.put(currentQName, collapsedValue); // Reset scratch.clear(); } if (nextQNameId != null) { // Add to the current entries scratch.put(nextPropertyKey, nextPropertyValue); currentQNameId = nextQNameId; } else { // There is no next value to process break; } } // Done return propertyMap; } private Serializable collapsePropertiesWithSameQName(PropertyDefinition propertyDef, SortedMap<NodePropertyKey, NodePropertyValue> sortedPropertyValues) { Serializable result = null; Collection<Serializable> collectionResult = null; // A working map. Ordering is not important for this map. Map<NodePropertyKey, NodePropertyValue> scratch = new HashMap<NodePropertyKey, NodePropertyValue>(3); // Iterate (sorted) over the map entries and extract values with the same list index Integer currentListIndex = Integer.MIN_VALUE; Iterator<Map.Entry<NodePropertyKey, NodePropertyValue>> iterator = sortedPropertyValues.entrySet() .iterator(); while (true) { Integer nextListIndex = null; NodePropertyKey nextPropertyKey = null; NodePropertyValue nextPropertyValue = null; // Record the next entry's values if (iterator.hasNext()) { Map.Entry<NodePropertyKey, NodePropertyValue> entry = iterator.next(); nextPropertyKey = entry.getKey(); nextPropertyValue = entry.getValue(); nextListIndex = nextPropertyKey.getListIndex(); } // If the list index is going to change, and we have some entries to process, then process them. if (scratch.size() > 0 && (nextListIndex == null || !nextListIndex.equals(currentListIndex))) { // We have added something to the scratch properties but the index has just changed Serializable collapsedValue = collapsePropertiesWithSameQNameAndListIndex(propertyDef, scratch); // Store. If there is a value already, then we must build a collection. if (result == null) { result = collapsedValue; } else if (collectionResult != null) { // We have started a collection, so just add the value to it. collectionResult.add(collapsedValue); } else { // We already had a result, and now have another. A collection has not been // started. We start a collection and explicitly keep track of it so that // we don't get mixed up with collections of collections (ETHREEOH-2064). collectionResult = new ArrayList<Serializable>(20); collectionResult.add(result); // Add the first result collectionResult.add(collapsedValue); // Add the new value result = (Serializable) collectionResult; } // Reset scratch.clear(); } if (nextListIndex != null) { // Add to the current entries scratch.put(nextPropertyKey, nextPropertyValue); currentListIndex = nextListIndex; } else { // There is no next value to process break; } } // Make sure that multi-valued properties are returned as a collection if (propertyDef != null && propertyDef.isMultiValued() && result != null && !(result instanceof Collection<?>)) { // Can't use Collections.singletonList: ETHREEOH-1172 ArrayList<Serializable> collection = new ArrayList<Serializable>(1); collection.add(result); result = collection; } // Done return result; } /** * At this level, the properties have the same qname and list index. They can only be separated by locale. * Typically, MLText will fall into this category as only. * <p> * If there are multiple values then they can only be separated by locale. If they are separated by locale, then * they have to be text-based. This means that the only way to store them is via MLText. Any other multi-locale * properties cannot be deserialized. */ private Serializable collapsePropertiesWithSameQNameAndListIndex(PropertyDefinition propertyDef, Map<NodePropertyKey, NodePropertyValue> propertyValues) { int propertyValuesSize = propertyValues.size(); Serializable value = null; if (propertyValuesSize == 0) { // Nothing to do return value; } // Do we definitely have MLText? boolean isMLText = (propertyDef != null && propertyDef.getDataType().getName().equals(DataTypeDefinition.MLTEXT)); // Determine the default locale ID. The chance of it being null is vanishingly small, but ... Pair<Long, Locale> defaultLocalePair = localeDAO.getDefaultLocalePair(); Long defaultLocaleId = (defaultLocalePair == null) ? null : defaultLocalePair.getFirst(); Integer listIndex = null; for (Map.Entry<NodePropertyKey, NodePropertyValue> entry : propertyValues.entrySet()) { NodePropertyKey propertyKey = entry.getKey(); NodePropertyValue propertyValue = entry.getValue(); // Check that the client code has gathered the values together correctly if (listIndex == null) { listIndex = propertyKey.getListIndex(); } else if (!listIndex.equals(propertyKey.getListIndex())) { throw new IllegalStateException( "Expecting to collapse properties with same list index: " + propertyValues); } // Get the locale of the current value Long localeId = propertyKey.getLocaleId(); boolean isDefaultLocale = EqualsHelper.nullSafeEquals(defaultLocaleId, localeId); // Get the local entry value Serializable entryValue = makeSerializableValue(propertyDef, propertyValue); // A default locale indicates a simple value i.e. the entry represents the whole value, // unless the dictionary specifically declares it to be d:mltext if (isDefaultLocale && !isMLText) { // Check and warn if there are other values if (propertyValuesSize > 1) { logger.warn("Found localized properties along with a 'null' value in the default locale. \n" + " The localized values will be ignored; 'null' will be returned: \n" + " Default locale ID: " + defaultLocaleId + "\n" + " Property: " + propertyDef + "\n" + " Values: " + propertyValues); } // The entry could be null or whatever value came out value = entryValue; break; } else { // Non-default locales indicate MLText ONLY. Locale locale = localeDAO.getLocalePair(localeId).getSecond(); // Note that we force a non-null value here as a null MLText object is persisted // just like any other null i.e. with the default locale. if (value == null) { value = new MLText(); } // We break for other entry values, so no need to check the non-null case // Put the current value into the MLText object if (entryValue == null || entryValue instanceof String) { // Can put in nulls and Strings ((MLText) value).put(locale, (String) entryValue); // We've checked the casts } else { // It's a non-null non-String ... can't be added to MLText! logger.warn("Found localized non-String properties. \n" + " The non-String values will be ignored: \n" + " Default locale ID: " + defaultLocaleId + "\n" + " Property: " + propertyDef + "\n" + " Values: " + propertyValues); } } } // Done return value; } /** * Extracts the externally-visible property from the persistable value. * * @param propertyDef the model property definition - may be <tt>null</tt> * @param propertyValue the persisted property * @return Returns the value of the property in the format dictated by the property definition, * or null if the property value is null */ public Serializable makeSerializableValue(PropertyDefinition propertyDef, NodePropertyValue propertyValue) { if (propertyValue == null) { return null; } // get property attributes final QName propertyTypeQName; if (propertyDef == null) { // allow this for now propertyTypeQName = DataTypeDefinition.ANY; } else { propertyTypeQName = propertyDef.getDataType().getName(); } try { Serializable value = propertyValue.getValue(propertyTypeQName); // Handle conversions to and from ContentData if (value instanceof ContentDataId) { // ContentData used to be persisted as a String and then as a Long. // Now it has a special type to denote the ID Long contentDataId = ((ContentDataId) value).getId(); ContentData contentData = contentDataDAO.getContentData(contentDataId).getSecond(); value = new ContentDataWithId(contentData, contentDataId); } else if ((value instanceof Long) && propertyTypeQName.equals(DataTypeDefinition.CONTENT)) { Long contentDataId = (Long) value; ContentData contentData = contentDataDAO.getContentData(contentDataId).getSecond(); value = new ContentDataWithId(contentData, contentDataId); } // done return value; } catch (TypeConversionException e) { throw new TypeConversionException( "The property value is not compatible with the type defined for the property: \n" + " property: " + (propertyDef == null ? "unknown" : propertyDef) + "\n" + " property value: " + propertyValue, e); } } }