/*
 * #%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.propval;

import java.io.Serializable;
import java.sql.Savepoint;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.alfresco.repo.cache.NullCache;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
import org.alfresco.repo.domain.CrcHelper;
import org.alfresco.repo.domain.control.ControlDAO;
import org.alfresco.repo.domain.propval.PropertyValueEntity.PersistedType;
import org.alfresco.repo.domain.schema.SchemaBootstrap;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataIntegrityViolationException;

/**
 * Abstract implementation for Property Value DAO.
 * <p>
 * This provides basic services such as caching, but defers to the underlying implementation
 * for CRUD operations. 
 * 
 * @author Derek Hulley
 * @since 3.2
 */
public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
{
    private static final String CACHE_REGION_PROPERTY_CLASS = "PropertyClass";
    private static final String CACHE_REGION_PROPERTY_DATE_VALUE = "PropertyDateValue";
    private static final String CACHE_REGION_PROPERTY_STRING_VALUE = "PropertyStringValue";
    private static final String CACHE_REGION_PROPERTY_DOUBLE_VALUE = "PropertyDoubleValue";
    private static final String CACHE_REGION_PROPERTY_SERIALIZABLE_VALUE = "PropertySerializableValue";
    private static final String CACHE_REGION_PROPERTY_VALUE = "PropertyValue";
    private static final String CACHE_REGION_PROPERTY = "Property";
    
    protected final Log logger = LogFactory.getLog(getClass());
    
    protected PropertyTypeConverter converter;
    protected ControlDAO controlDAO;
    
    private final PropertyClassCallbackDAO propertyClassDaoCallback;
    private final PropertyDateValueCallbackDAO propertyDateValueCallback;
    private final PropertyStringValueCallbackDAO propertyStringValueCallback;
    private final PropertyDoubleValueCallbackDAO propertyDoubleValueCallback;
    private final PropertySerializableValueCallbackDAO propertySerializableValueCallback;
    private final PropertyValueCallbackDAO propertyValueCallback;
    private final PropertyCallbackDAO propertyCallback;
    /**
     * Cache for the property class:<br/>
     * KEY: ID<br/>
     * VALUE: Java class<br/>
     * VALUE KEY: Java class name<br/>
     */
    private EntityLookupCache<Long, Class<?>, String> propertyClassCache;
    /**
     * Cache for the property date value:<br/>
     * KEY: ID<br/>
     * VALUE: The Date instance<br/>
     * VALUE KEY: The date-only date (i.e. everything below day is zeroed)<br/>
     */
    private EntityLookupCache<Long, Date, Date> propertyDateValueCache;
    /**
     * Cache for the property string value:<br/>
     * KEY: ID<br/>
     * VALUE: The full string<br/>
     * VALUE KEY: Short string-crc pair ({@link CrcHelper#getStringCrcPair(String, int, boolean, boolean)})<br/>
     */
    private EntityLookupCache<Long, String, Pair<String, Long>> propertyStringValueCache;
    /**
     * Cache for the property double value:<br/>
     * KEY: ID<br/>
     * VALUE: The Double instance<br/>
     * VALUE KEY: The value itself<br/>
     */
    private EntityLookupCache<Long, Double, Double> propertyDoubleValueCache;
    /**
     * Cache for the property Serializable value:<br/>
     * KEY: ID<br/>
     * VALUE: The Serializable instance<br/>
     * VALUE KEY: none<br/>.  The cache is not used for value-based lookups.
     */
    private EntityLookupCache<Long, Serializable, Serializable> propertySerializableValueCache;
    /**
     * Cache for the property value:<br/>
     * KEY: ID<br/>
     * VALUE: The Serializable instance<br/>
     * VALUE KEY: A value key based on the persisted type<br/>
     */
    private EntityLookupCache<Long, Serializable, Serializable> propertyValueCache;
    /**
     * Cache for the property:<br/>
     * KEY: ID<br/>
     * VALUE: The Serializable instance<br/>
     * VALUE KEY: A value key based on the persisted type<br/>
     */
    private EntityLookupCache<Long, Serializable, Serializable> propertyCache;
    
    private SimpleCache<CachePucKey, PropertyUniqueContextEntity> propertyUniqueContextCache; // cluster-aware
    
    /**
     * Flag to throw exception if type of the key doesn't guarantee uniqueness, @see MNT-11895
     */
    private boolean uniquenessCheckEnabled = true;
    
    /**
     * Setter for uniquenessCheckEnabled flag
     */
    public void setUniquenessCheckEnabled(boolean uniquenessCheckEnabled)
    {
        this.uniquenessCheckEnabled = uniquenessCheckEnabled;
    }

    /**
     * Set the cache to use for unique property lookups
     */
    public void setPropertyUniqueContextCache(SimpleCache<CachePucKey, PropertyUniqueContextEntity> propertyUniqueContextCache)
    {
        this.propertyUniqueContextCache = propertyUniqueContextCache;
    }
    
    
    /**
     * Default constructor.
     * <p>
     * This sets up the DAO accessors to bypass any caching to handle the case where the caches are not
     * supplied in the setters.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public AbstractPropertyValueDAOImpl()
    {
        this.propertyClassDaoCallback = new PropertyClassCallbackDAO();
        this.propertyDateValueCallback = new PropertyDateValueCallbackDAO();
        this.propertyStringValueCallback = new PropertyStringValueCallbackDAO();
        this.propertyDoubleValueCallback = new PropertyDoubleValueCallbackDAO();
        this.propertySerializableValueCallback = new PropertySerializableValueCallbackDAO();
        this.propertyValueCallback = new PropertyValueCallbackDAO();
        this.propertyCallback = new PropertyCallbackDAO();
        
        this.propertyClassCache = new EntityLookupCache<Long, Class<?>, String>(propertyClassDaoCallback);
        this.propertyDateValueCache = new EntityLookupCache<Long, Date, Date>(propertyDateValueCallback);
        this.propertyStringValueCache = new EntityLookupCache<Long, String, Pair<String, Long>>(propertyStringValueCallback);
        this.propertyDoubleValueCache = new EntityLookupCache<Long, Double, Double>(propertyDoubleValueCallback);
        this.propertySerializableValueCache = new EntityLookupCache<Long, Serializable, Serializable>(propertySerializableValueCallback);
        this.propertyValueCache = new EntityLookupCache<Long, Serializable, Serializable>(propertyValueCallback);
        this.propertyCache = new EntityLookupCache<Long, Serializable, Serializable>(propertyCallback);
        
        this.propertyUniqueContextCache = (SimpleCache<CachePucKey, PropertyUniqueContextEntity>)new NullCache();
    }

    /**
     * @param converter                     the converter that translates between external and persisted values
     */
    public void setConverter(PropertyTypeConverter converter)
    {
        this.converter = converter;
    }

    /**
     * @param controlDAO                    the DAO that provides connection control
     */
    public void setControlDAO(ControlDAO controlDAO)
    {
        this.controlDAO = controlDAO;
    }

    /**
     * Set the cache to use for <b>alf_prop_class</b> lookups (optional).
     * 
     * @param propertyClassCache            the cache of IDs to property classes
     */
    public void setPropertyClassCache(SimpleCache<Serializable, Object> propertyClassCache)
    {
        this.propertyClassCache = new EntityLookupCache<Long, Class<?>, String>(
                propertyClassCache,
                CACHE_REGION_PROPERTY_CLASS,
                propertyClassDaoCallback);
    }
    
    /**
     * Set the cache to use for <b>alf_prop_date_value</b> lookups (optional).
     * 
     * @param propertyDateValueCache        the cache of IDs to property values
     */
    public void setPropertyDateValueCache(SimpleCache<Serializable, Object> propertyDateValueCache)
    {
        this.propertyDateValueCache = new EntityLookupCache<Long, Date, Date>(
                propertyDateValueCache,
                CACHE_REGION_PROPERTY_DATE_VALUE,
                propertyDateValueCallback);
    }
    
    /**
     * Set the cache to use for <b>alf_prop_string_value</b> lookups (optional).
     * 
     * @param propertyStringValueCache      the cache of IDs to property string values
     */
    public void setPropertyStringValueCache(SimpleCache<Serializable, Object> propertyStringValueCache)
    {
        this.propertyStringValueCache = new EntityLookupCache<Long, String, Pair<String, Long>>(
                propertyStringValueCache,
                CACHE_REGION_PROPERTY_STRING_VALUE,
                propertyStringValueCallback);
    }
    
    /**
     * Set the cache to use for <b>alf_prop_double_value</b> lookups (optional).
     * 
     * @param propertyDoubleValueCache     the cache of IDs to property values
     */
    public void setPropertyDoubleValueCache(SimpleCache<Serializable, Object> propertyDoubleValueCache)
    {
        this.propertyDoubleValueCache = new EntityLookupCache<Long, Double, Double>(
                propertyDoubleValueCache,
                CACHE_REGION_PROPERTY_DOUBLE_VALUE,
                propertyDoubleValueCallback);
    }
    
    /**
     * Set the cache to use for <b>alf_prop_serializable_value</b> lookups (optional).
     * 
     * @param propertySerializableValueCache     the cache of IDs to property values
     */
    public void setPropertySerializableValueCache(SimpleCache<Serializable, Object> propertySerializableValueCache)
    {
        this.propertySerializableValueCache = new EntityLookupCache<Long, Serializable, Serializable>(
                propertySerializableValueCache,
                CACHE_REGION_PROPERTY_SERIALIZABLE_VALUE,
                propertySerializableValueCallback);
    }
    
    /**
     * Set the cache to use for <b>alf_prop_value</b> lookups (optional).
     * 
     * @param propertyValueCache     the cache of IDs to property values
     */
    public void setPropertyValueCache(SimpleCache<Serializable, Object> propertyValueCache)
    {
        this.propertyValueCache = new EntityLookupCache<Long, Serializable, Serializable>(
                propertyValueCache,
                CACHE_REGION_PROPERTY_VALUE,
                propertyValueCallback);
    }
    
    /**
     * Set the cache to use for <b>alf_prop_root</b> lookups (optional).
     * 
     * @param propertyCache     the cache of IDs to property values
     */
    public void setPropertyCache(SimpleCache<Serializable, Object> propertyCache)
    {
        this.propertyCache = new EntityLookupCache<Long, Serializable, Serializable>(
                propertyCache,
                CACHE_REGION_PROPERTY,
                propertyCallback);
    }
    
    //================================
    // 'alf_prop_class' accessors
    //================================

    public Pair<Long, Class<?>> getPropertyClassById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, Class<?>> entityPair = propertyClassCache.getByKey(id);
        if (entityPair == null)
        {
            throw new DataIntegrityViolationException("No property class exists for ID " + id);
        }
        return entityPair;
    }

    public Pair<Long, Class<?>> getPropertyClass(Class<?> value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Property class cannot be null");
        }
        Pair<Long, Class<?>> entityPair = propertyClassCache.getByValue(value);
        return entityPair;
    }

    public Pair<Long, Class<?>> getOrCreatePropertyClass(Class<?> value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Property class cannot be null");
        }
        Pair<Long, Class<?>> entityPair = propertyClassCache.getOrCreateByValue(value);
        return entityPair;
    }

    /**
     * Callback for <b>alf_prop_class</b> DAO.
     */
    private class PropertyClassCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Class<?>, String>
    {
        private final Pair<Long, Class<?>> convertEntityToPair(PropertyClassEntity entity)
        {
            if (entity == null)
            {
                return null;
            }
            else
            {
                return entity.getEntityPair();
            }
        }
        
        public String getValueKey(Class<?> value)
        {
            return value.getName();
        }

        public Pair<Long, Class<?>> createValue(Class<?> value)
        {
            PropertyClassEntity entity = createClass(value);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Class<?>> findByKey(Long key)
        {
            PropertyClassEntity entity = findClassById(key);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Class<?>> findByValue(Class<?> value)
        {
            PropertyClassEntity entity = findClassByValue(value);
            return convertEntityToPair(entity);
        }
    }
    
    protected abstract PropertyClassEntity findClassById(Long id);
    protected abstract PropertyClassEntity findClassByValue(Class<?> value);
    protected abstract PropertyClassEntity createClass(Class<?> value);
    
    //================================
    // 'alf_prop_date_value' accessors
    //================================

    public Pair<Long, Date> getPropertyDateValueById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, Date> entityPair = propertyDateValueCache.getByKey(id);
        if (entityPair == null)
        {
            throw new DataIntegrityViolationException("No property date value exists for ID " + id);
        }
        return entityPair;
    }

    public Pair<Long, Date> getPropertyDateValue(Date value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted date values cannot be null");
        }
        value = PropertyDateValueEntity.truncateDate(value);
        Pair<Long, Date> entityPair = propertyDateValueCache.getByValue(value);
        return entityPair;
    }

    public Pair<Long, Date> getOrCreatePropertyDateValue(Date value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted date values cannot be null");
        }
        value = PropertyDateValueEntity.truncateDate(value);
        Pair<Long, Date> entityPair = propertyDateValueCache.getOrCreateByValue(value);
        return (Pair<Long, Date>) entityPair;
    }

    /**
     * Callback for <b>alf_prop_date_value</b> DAO.
     */
    private class PropertyDateValueCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Date, Date>
    {
        private final Pair<Long, Date> convertEntityToPair(PropertyDateValueEntity entity)
        {
            if (entity == null)
            {
                return null;
            }
            else
            {
                return entity.getEntityPair();
            }
        }
        
        /**
         * {@inheritDoc}
         * <p/>
         * The value will already have been truncated to be accurate to the last day
         */
        public Date getValueKey(Date value)
        {
            return PropertyDateValueEntity.truncateDate(value);
        }

        public Pair<Long, Date> createValue(Date value)
        {
            PropertyDateValueEntity entity = createDateValue(value);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Date> findByKey(Long key)
        {
            PropertyDateValueEntity entity = findDateValueById(key);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Date> findByValue(Date value)
        {
            PropertyDateValueEntity entity = findDateValueByValue(value);
            return convertEntityToPair(entity);
        }
    }
    
    protected abstract PropertyDateValueEntity findDateValueById(Long id);
    /**
     * @param value             a date, accurate to the day
     */
    protected abstract PropertyDateValueEntity findDateValueByValue(Date value);
    /**
     * @param value             a date, accurate to the day
     */
    protected abstract PropertyDateValueEntity createDateValue(Date value);

    //================================
    // 'alf_prop_string_value' accessors
    //================================

    public Pair<String, Long> getPropertyStringCaseSensitiveSearchParameters(String value)
    {
        return CrcHelper.getStringCrcPair(value, 16, false, true);
    }

    public Pair<Long, String> getPropertyStringValueById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, String> entityPair = propertyStringValueCache.getByKey(id);
        if (entityPair == null)
        {
            throw new DataIntegrityViolationException("No property string value exists for ID " + id);
        }
        return entityPair;
    }

    public Pair<Long, String> getPropertyStringValue(String value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted string values cannot be null");
        }
        Pair<Long, String> entityPair = propertyStringValueCache.getByValue(value);
        return entityPair;
    }

    public Pair<Long, String> getOrCreatePropertyStringValue(String value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted string values cannot be null");
        }
        int maxStringLen = SchemaBootstrap.getMaxStringLength();
        if (value.length() > maxStringLen)
        {
            throw new IllegalArgumentException(
                    "Persisted string values for 'alf_prop_string_value' cannot be longer than "
                    + maxStringLen + " characters.  Increase the string column sizes and set property " +
                    "'system.maximumStringLength' accordingly.");
        }
        Pair<Long, String> entityPair = propertyStringValueCache.getOrCreateByValue(value);
        return entityPair;
    }

    /**
     * Callback for <b>alf_prop_string_value</b> DAO.
     */
    private class PropertyStringValueCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, String, Pair<String, Long>>
    {
        public Pair<String, Long> getValueKey(String value)
        {
            return getPropertyStringCaseSensitiveSearchParameters(value);
        }

        public Pair<Long, String> createValue(String value)
        {
            Long key = createStringValue(value);
            return new Pair<Long, String>(key, value);
        }

        public Pair<Long, String> findByKey(Long key)
        {
            String value = findStringValueById(key);
            if (value == null)
            {
                return null;
            }
            else
            {
                return new Pair<Long, String>(key, value);
            }
        }

        public Pair<Long, String> findByValue(String value)
        {
            Long key = findStringValueByValue(value);
            if (key == null)
            {
                return null;
            }
            else
            {
                return new Pair<Long, String>(key, value);
            }
        }
    }
    
    protected abstract String findStringValueById(Long id);
    protected abstract Long findStringValueByValue(String value);
    protected abstract Long createStringValue(String value);

    //================================
    // 'alf_prop_double_value' accessors
    //================================

    public Pair<Long, Double> getPropertyDoubleValueById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, Double> entityPair = propertyDoubleValueCache.getByKey(id);
        if (entityPair == null)
        {
            throw new DataIntegrityViolationException("No property double value exists for ID " + id);
        }
        return entityPair;
    }

    public Pair<Long, Double> getPropertyDoubleValue(Double value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted double values cannot be null");
        }
        Pair<Long, Double> entityPair = propertyDoubleValueCache.getByValue(value);
        return entityPair;
    }

    public Pair<Long, Double> getOrCreatePropertyDoubleValue(Double value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted double values cannot be null");
        }
        Pair<Long, Double> entityPair = propertyDoubleValueCache.getOrCreateByValue(value);
        return (Pair<Long, Double>) entityPair;
    }

    /**
     * Callback for <b>alf_prop_double_value</b> DAO.
     */
    private class PropertyDoubleValueCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Double, Double>
    {
        private final Pair<Long, Double> convertEntityToPair(PropertyDoubleValueEntity entity)
        {
            if (entity == null)
            {
                return null;
            }
            else
            {
                return entity.getEntityPair();
            }
        }
        
        public Double getValueKey(Double value)
        {
            return value;
        }

        public Pair<Long, Double> createValue(Double value)
        {
            PropertyDoubleValueEntity entity = createDoubleValue(value);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Double> findByKey(Long key)
        {
            PropertyDoubleValueEntity entity = findDoubleValueById(key);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Double> findByValue(Double value)
        {
            PropertyDoubleValueEntity entity = findDoubleValueByValue(value);
            return convertEntityToPair(entity);
        }
    }
    
    protected abstract PropertyDoubleValueEntity findDoubleValueById(Long id);
    protected abstract PropertyDoubleValueEntity findDoubleValueByValue(Double value);
    protected abstract PropertyDoubleValueEntity createDoubleValue(Double value);

    //================================
    // 'alf_prop_serializable_value' accessors
    //================================

    public Pair<Long, Serializable> getPropertySerializableValueById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, Serializable> entityPair = propertySerializableValueCache.getByKey(id);
        if (entityPair == null)
        {
            throw new DataIntegrityViolationException("No property serializable value exists for ID " + id);
        }
        return entityPair;
    }

    public Pair<Long, Serializable> createPropertySerializableValue(Serializable value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("Persisted serializable values cannot be null");
        }
        Pair<Long, Serializable> entityPair = propertySerializableValueCache.getOrCreateByValue(value);
        return (Pair<Long, Serializable>) entityPair;
    }

    /**
     * Callback for <b>alf_prop_serializable_value</b> DAO.
     */
    private class PropertySerializableValueCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Serializable, Serializable>
    {
        private final Pair<Long, Serializable> convertEntityToPair(PropertySerializableValueEntity entity)
        {
            if (entity == null)
            {
                return null;
            }
            else
            {
                return entity.getEntityPair();
            }
        }
        
        public Pair<Long, Serializable> createValue(Serializable value)
        {
            PropertySerializableValueEntity entity = createSerializableValue(value);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Serializable> findByKey(Long key)
        {
            PropertySerializableValueEntity entity = findSerializableValueById(key);
            return convertEntityToPair(entity);
        }
    }
    
    protected abstract PropertySerializableValueEntity findSerializableValueById(Long id);
    protected abstract PropertySerializableValueEntity createSerializableValue(Serializable value);

    //================================
    // 'alf_prop_value' accessors
    //================================

    public Pair<Long, Serializable> getPropertyValueById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, Serializable> entityPair = propertyValueCache.getByKey(id);
        if (entityPair == null)
        {
            throw new DataIntegrityViolationException("No property value exists for ID " + id);
        }
        return entityPair;
    }

    public Pair<Long, Serializable> getPropertyValue(Serializable value)
    {
        Pair<Long, Serializable> entityPair = propertyValueCache.getByValue(value);
        return entityPair;
    }

    public Pair<Long, Serializable> getOrCreatePropertyValue(Serializable value)
    {
        Pair<Long, Serializable> entityPair = propertyValueCache.getOrCreateByValue(value);
        return (Pair<Long, Serializable>) entityPair;
    }

    /**
     * Callback for <b>alf_prop_value</b> DAO.
     */
    private class PropertyValueCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Serializable, Serializable>
    {
        @SuppressWarnings("unchecked")
        private final Serializable convertToValue(PropertyValueEntity entity)
        {
            if (entity == null)
            {
                return null;
            }
            Long actualTypeId = entity.getActualTypeId();
            final Class<Serializable> actualType = (Class<Serializable>) getPropertyClassById(actualTypeId).getSecond();
            final Serializable actualValue = entity.getValue(actualType, converter);
            // Done
            return actualValue;
        }
        
        private final Pair<Long, Serializable> convertEntityToPair(PropertyValueEntity entity)
        {
            if (entity == null)
            {
                return null;
            }
            Long entityId = entity.getId();
            Serializable actualValue = convertToValue(entity);
            // Done
            return new Pair<Long, Serializable>(entityId, actualValue);
        }
        
        public Serializable getValueKey(Serializable value)
        {
            PersistedType persistedType = PropertyValueEntity.getPersistedTypeEnum(value, converter);
            // We don't return keys for pure Serializable instances
            if (persistedType == PersistedType.SERIALIZABLE)
            {
                // It will be Serialized, so no search key
                return null;
            }
            else if (value instanceof Map<?, ?> && ((Map) value).isEmpty())
            {
                // See MNT-20992 - Maps and their subclasses do not have a good enough equals method for use as a key
                // in caches. For example empty HashMap "equals" empty MLText
                return null;
            }
            else if (value instanceof String)
            {
                return CrcHelper.getStringCrcPair((String)value, 128, true, true);
            }
            else
            {
                // We've dodged Serializable and String; everything else is OK as a key.
                return value;
            }
        }

        public Pair<Long, Serializable> createValue(Serializable value)
        {
            PropertyValueEntity entity = createPropertyValue(value);
            // Done
            return new Pair<Long, Serializable>(entity.getId(), value);
        }

        public Pair<Long, Serializable> findByKey(Long key)
        {
            PropertyValueEntity entity = findPropertyValueById(key);
            return convertEntityToPair(entity);
        }

        public Pair<Long, Serializable> findByValue(Serializable value)
        {
            PropertyValueEntity entity = findPropertyValueByValue(value);
            return convertEntityToPair(entity);
        }

        /**
         * No-op.  This is implemented as we just want to update the cache.
         * @return              Returns 0 always
         */
        @Override
        public int updateValue(Long key, Serializable value)
        {
            return 0;
        }
    }
    
    protected abstract PropertyValueEntity findPropertyValueById(Long id);
    protected abstract PropertyValueEntity findPropertyValueByValue(Serializable value);
    protected abstract PropertyValueEntity createPropertyValue(Serializable value);

    //================================
    // 'alf_prop_root' accessors
    //================================

    public Serializable getPropertyById(Long id)
    {
        if (id == null)
        {
            throw new IllegalArgumentException("Cannot look up entity by null ID.");
        }
        Pair<Long, Serializable> entityPair = propertyCache.getByKey(id);
        if (entityPair == null)
        {
            // Remove from cache
            propertyCache.removeByKey(id);
            
            throw new DataIntegrityViolationException("No property value exists for ID " + id);
        }
        return entityPair.getSecond();
    }

    public void getPropertiesByIds(List<Long> ids, PropertyFinderCallback callback)
    {
        findPropertiesByIds(ids, callback);
    }

    /**
     * {@inheritDoc}
     * @see #createPropertyImpl(Long, long, long, Long, Serializable)
     */
    public Long createProperty(Serializable value)
    {
        Pair<Long, Serializable> entityPair = propertyCache.getOrCreateByValue(value);
        return entityPair.getFirst();
    }
    
    public void updateProperty(Long rootPropId, Serializable value)
    {
        propertyCache.updateValue(rootPropId, value);
    }

    public void deleteProperty(Long id)
    {
        propertyCache.deleteByKey(id);
    }

    /**
     * Callback for <b>alf_prop_root</b> DAO.
     */
    private class PropertyCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, Serializable, Serializable>
    {
        public Pair<Long, Serializable> createValue(Serializable value)
        {
            // We will need a new root
            Long rootPropId = createPropertyRoot();
            createPropertyImpl(rootPropId, 0L, 0L, null, value);
            // Done
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Created property: \n" +
                        "   ID: " + rootPropId + "\n" +
                        "   Value: " + value);
            }
            return new Pair<Long, Serializable>(rootPropId, value);
        }

        public Pair<Long, Serializable> findByKey(Long key)
        {
            List<PropertyIdSearchRow> rows = findPropertyById(key);
            if (rows.size() == 0)
            {
                // No results
                return null;
            }
            Serializable value = convertPropertyIdSearchRows(rows);
            return new Pair<Long, Serializable>(key, value);
        }

        /**
         * Updates a property.  The <b>alf_prop_root</b> entity is updated
         * to ensure concurrent modification is detected.
         * 
         * @return              Returns 1 always
         */
        @Override
        public int updateValue(Long key, Serializable value)
        {
            // Remove all entries for the root
            PropertyRootEntity entity = getPropertyRoot(key);
            if (entity == null)
            {
                throw new DataIntegrityViolationException("No property root exists for ID " + key);
            }
            // Remove all links using the root
            deletePropertyLinks(key);
            // Create the new properties and update the cache
            createPropertyImpl(key, 0L, 0L, null, value);
            // Update the property root to detect concurrent modification
            updatePropertyRoot(entity);
            // Done
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Updated property: \n" +
                        "   ID: " + key + "\n" +
                        "   Value: " + value);
            }
            return 1;
        }

        @Override
        public int deleteByKey(Long key)
        {
            deletePropertyRoot(key);
            // Done
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Deleted property: \n" +
                        "   ID: " + key);
            }
            return 1;
        }
    }

    /**
     * @param propIndex         a unique index within the context of the current property root
     */
    @SuppressWarnings("unchecked")
    private long createPropertyImpl(
            Long rootPropId,
            long propIndex,
            long containedIn,
            Long keyPropId,
            Serializable value)
    {
        // Keep track of the index for this property.  It gets used later when making the link entry.
        long thisPropIndex = propIndex;
        
        Long valuePropId = null;
        if (value == null)
        {
            // The key and the value are the same
            valuePropId = getOrCreatePropertyValue(value).getFirst();
        }
        else if (value instanceof Map<?, ?>)
        {
            Map<Serializable, Serializable> map = (Map<Serializable, Serializable>) value;
            // Check if the it has a default constructor
            Serializable emptyInstance = constructEmptyContainer(value.getClass());
            if (emptyInstance == null)
            {
                // No default constructor, so we just throw the whole thing in as a single property
                valuePropId = getOrCreatePropertyValue(value).getFirst();
            }
            else
            {
                // Persist the empty map
                valuePropId = getOrCreatePropertyValue(emptyInstance).getFirst();
                // Persist the individual entries
                for (Map.Entry<Serializable, Serializable> entry : map.entrySet())
                {
                    // Recurse for each value
                    Serializable mapKey = entry.getKey();
                    Serializable mapValue = entry.getValue();
                    // Get the IDs for these
                    Long mapKeyId = getOrCreatePropertyValue(mapKey).getFirst();
                    propIndex = createPropertyImpl(
                            rootPropId,
                            propIndex + 1L,
                            thisPropIndex,
                            mapKeyId,
                            mapValue);
                }
            }
        }
        else if (value instanceof Collection<?>)
        {
            Collection<Serializable> collection = (Collection<Serializable>) value;
            // Check if the it has a default constructor
            Serializable emptyInstance = constructEmptyContainer(value.getClass());
            if (emptyInstance == null)
            {
                // No default constructor, so we just throw the whole thing in as a single property
                valuePropId = getOrCreatePropertyValue(value).getFirst();
            }
            else
            {
                // Persist the empty collection
                valuePropId = getOrCreatePropertyValue(emptyInstance).getFirst();
                // Persist the individual entries
                for (Serializable collectionValue : collection)
                {
                    // Recurse for each value
                    propIndex = createPropertyImpl(
                            rootPropId,
                            propIndex + 1L,
                            thisPropIndex,
                            null,
                            collectionValue);
                }
            }
        }
        else
        {
            // The key and the value are the same
            valuePropId = getOrCreatePropertyValue(value).getFirst();
        }
        
        // Create a link entry
        if (keyPropId == null)
        {
            // If the key matches the value then it is the root
            keyPropId = valuePropId;
        }
        createPropertyLink(rootPropId, thisPropIndex, containedIn, keyPropId, valuePropId);
        
        // Done
        return propIndex;
    }
    
    private static final Serializable EMPTY_HASHMAP = new HashMap<Serializable, Serializable>();
    private static final Serializable EMPTY_LIST = new ArrayList<Serializable>();
    private static final Serializable EMPTY_SET = new HashSet<Serializable>();

    /**
     * Returns a reconstructable instance 
     * 
     * @return          Returns an empty instance of the given container (map or collection), or
     *                  <tt>null</tt> if it is not possible to do 
     */
    protected Serializable constructEmptyContainer(Class<?> clazz)
    {
        try
        {
            return (Serializable) clazz.getConstructor().newInstance();
        }
        catch (Throwable e)
        {
            // Can't be constructed, so we just choose a well-known implementation.
            // There are so many variations on maps and collections (Unmodifiable, Immutable, etc)
            // that to not choose an alternative would leave the database full of BLOBs
        }
        if (Map.class.isAssignableFrom(clazz))
        {
            return EMPTY_HASHMAP;
        }
        else if (List.class.isAssignableFrom(clazz))
        {
            return EMPTY_LIST;
        }
        else if (Set.class.isAssignableFrom(clazz))
        {
            return EMPTY_SET;
        }
        else
        {
            logger.warn("Unable to find suitable container type with default constructor: " + clazz);
            return null;
        }
    }
    
    protected abstract List<PropertyIdSearchRow> findPropertyById(Long id);
    protected abstract void findPropertiesByIds(List<Long> ids, PropertyFinderCallback callback);
    protected abstract Long createPropertyRoot();
    protected abstract PropertyRootEntity getPropertyRoot(Long id);
    protected abstract PropertyRootEntity updatePropertyRoot(PropertyRootEntity entity);
    protected abstract void deletePropertyRoot(Long id);
    
    /**
     * Create an entry for the map or collection link.
     * 
     * @param rootPropId            the root (entry-point) property ID
     * @param propIndex             the property number within the root property
     * @param containedIn           the property that contains the current value
     * @param keyPropId             the map key entity ID or collection position count
     * @param valuePropId           the ID of the entity storing the value (may be another map or collection)
     */
    protected abstract void createPropertyLink(
            Long rootPropId,
            Long propIndex,
            Long containedIn,
            Long keyPropId,
            Long valuePropId);
    
    /**
     * Remove all property links for a given property root.
     * 
     * @param rootPropId            the root (entry-point) property ID
     */
    protected abstract int deletePropertyLinks(Long rootPropId);
    
    //================================
    // 'alf_prop_unique_ctx' accessors
    //================================
    
    private CachePucKey getPucKey(Long id1, Long id2, Long id3)
    {
        return new CachePucKey(id1, id2, id3);
    }
    
    /**
     * Key for PropertyUniqueContext cache
     */
    public static class CachePucKey implements Serializable
    {
        private static final long serialVersionUID = -4294324585692613101L;
        
        private final Long key1;
        private final Long key2;
        private final Long key3;
        
        private final int hashCode;
        
        private CachePucKey(Long key1, Long key2, Long key3)
        {
            this.key1 = key1;
            this.key2 = key2;
            this.key3 = key3;
            this.hashCode = (key1 == null ? 0 : key1.hashCode()) + (key2 == null ? 0 : key2.hashCode()) + (key3 == null ? 0 : key3.hashCode());
        }
        
        @Override
        public String toString()
        {
            return key1 + "." + key2 + "." + key3;
        }
        
        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
            {
                return true;
            }
            else if (!(obj instanceof CachePucKey))
            {
                return false;
            }
            CachePucKey that = (CachePucKey) obj;
            return EqualsHelper.nullSafeEquals(this.key1, that.key1) && 
                   EqualsHelper.nullSafeEquals(this.key2, that.key2) &&
                   EqualsHelper.nullSafeEquals(this.key3, that.key3);
        }
        
        @Override
        public int hashCode()
        {
            return hashCode;
        }
    }
    
    private void checkUniquenessGuarantee(Serializable... values)
    {
        for (int i = 0; i < values.length; i++)
        {
            PersistedType persistedType = PropertyValueEntity.getPersistedTypeEnum(values[i], converter);
            if (persistedType == PersistedType.SERIALIZABLE)
            {
                if (uniquenessCheckEnabled)
                {
                    throw new IllegalArgumentException("Type of the KEY-" + i + " (" + values[i].getClass() + ") cannot guarantee uniqueness. " +
                       "Please, see https://issues.alfresco.com/jira/browse/MNT-11895 for details. " +
                       "Set system.propval.uniquenessCheck.enabled=false to not throw the exception.");
                }
                else
                {
                    logger.warn("Type of the KEY-" + i + " (" + values[i].getClass() + ") cannot guarantee uniqueness. " +
                            "Please, see https://issues.alfresco.com/jira/browse/MNT-11895 for details. " +
                            "Set system.propval.uniquenessCheck.enabled=true to throw the exception.");
                }
            }
        }
    }
    
    public Pair<Long, Long> createPropertyUniqueContext(
            Serializable value1, Serializable value2, Serializable value3,
            Serializable propertyValue1)
    {
        /*
         * Use savepoints so that the PropertyUniqueConstraintViolation can be caught and handled in-transaction
         */
        checkUniquenessGuarantee(value1, value2, value3);
        
        // Translate the properties.  Null values are acceptable
        Long id1 = getOrCreatePropertyValue(value1).getFirst();
        Long id2 = getOrCreatePropertyValue(value2).getFirst();
        Long id3 = getOrCreatePropertyValue(value3).getFirst();
        Long property1Id = null;
        if (propertyValue1 != null)
        {
            property1Id = createProperty(propertyValue1);
        }
        
        CachePucKey pucKey = getPucKey(id1, id2, id3);
        
        Savepoint savepoint = controlDAO.createSavepoint("createPropertyUniqueContext");
        try
        {
            PropertyUniqueContextEntity entity = createPropertyUniqueContext(id1, id2, id3, property1Id);
            controlDAO.releaseSavepoint(savepoint);
            
            // cache
            propertyUniqueContextCache.put(pucKey, entity);
            
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Created unique property context: \n" +
                        "   Values: " + value1 + "-" + value2 + "-" + value3 + "\n" +
                        "   Result: " + entity);
            }
            
            return new Pair<Long, Long>(entity.getId(), property1Id);
        }
        catch (Exception e)
        {
            // Remove from caches.  The individual values must also be removed in case they are incorrect.
            propertyValueCache.removeByValue(value1);
            propertyValueCache.removeByValue(value2);
            propertyValueCache.removeByValue(value3);
            propertyUniqueContextCache.remove(pucKey);
            
            controlDAO.rollbackToSavepoint(savepoint);
            throw new PropertyUniqueConstraintViolation(value1, value2, value3, e);
        }
    }
    
    public Pair<Long, Long> getPropertyUniqueContext(Serializable value1, Serializable value2, Serializable value3)
    {
        // Translate the properties.  Null values are quite acceptable
        Pair<Long, Serializable> pair1 = getPropertyValue(value1);
        Pair<Long, Serializable> pair2 = getPropertyValue(value2);
        Pair<Long, Serializable> pair3 = getPropertyValue(value3);
        if (pair1 == null || pair2 == null || pair3 == null)
        {
            // None of the values exist so no unique context values can exist
            return null;
        }
        Long id1 = pair1.getFirst();
        Long id2 = pair2.getFirst();
        Long id3 = pair3.getFirst();
        
        CachePucKey pucKey = getPucKey(id1, id2, id3);
        
        // check cache
        PropertyUniqueContextEntity entity = propertyUniqueContextCache.get(pucKey);
        if (entity == null)
        {
            // Remove from cache
            propertyUniqueContextCache.remove(pucKey);
            
            // query DB
            entity = getPropertyUniqueContextByValues(id1, id2, id3);
            
            if (entity != null)
            {
                // cache
                propertyUniqueContextCache.put(pucKey, entity);
            }
        }
         
        if ((entity != null) && (entity.getPropertyId() != null))
        {
            try
            {
                // eager fetch - ignore return for now (could change API)
                getPropertyById(entity.getPropertyId());
            }
            catch (DataIntegrityViolationException dive)
            {
            	// Remove from cache
                propertyUniqueContextCache.remove(pucKey);
                throw dive;
            }
        }
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Searched for unique property context: \n" +
                    "   Values: " + value1 + "-" + value2 + "-" + value3 + "\n" +
                    "   Result: " + entity);
        }
        return entity == null ? null : new Pair<Long, Long>(entity.getId(), entity.getPropertyId());
    }
    
    public void getPropertyUniqueContext(PropertyUniqueContextCallback callback, Serializable... values)
    {
        if (values.length < 1 || values.length > 3)
        {
            throw new IllegalArgumentException("Get of unique property sets must have 1, 2 or 3 values");
        }
        Long[] valueIds = new Long[values.length];
        for (int i = 0; i < values.length; i++)
        {
            Pair<Long, Serializable> valuePair = getPropertyValue(values[i]);
            if (valuePair == null)
            {
                // No such value, so no need to get
                return;
            }
            valueIds[i] = valuePair.getFirst();
        }
        
        // not cached
        getPropertyUniqueContextByValues(callback, valueIds);
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Searched for unique property context: \n" +
                    "   Values: " + Arrays.toString(values));
        }
    }
    
    /*
     * Update PUC keys - retain current property value
     * 
     */
    public void updatePropertyUniqueContextKeys(Long id, Serializable value1, Serializable value2, Serializable value3)
    {
        /*
         * Use savepoints so that the PropertyUniqueConstraintViolation can be caught and handled in-transactioin
         */
        
        // Translate the properties.  Null values are acceptable
        Long id1 = getOrCreatePropertyValue(value1).getFirst();
        Long id2 = getOrCreatePropertyValue(value2).getFirst();
        Long id3 = getOrCreatePropertyValue(value3).getFirst();
        
        CachePucKey pucKey = getPucKey(id1, id2, id3);
        
        Savepoint savepoint = controlDAO.createSavepoint("updatePropertyUniqueContext");
        try
        {
            PropertyUniqueContextEntity entity = getPropertyUniqueContextById(id);
            if (entity == null)
            {
                // Remove from cache
                propertyUniqueContextCache.remove(pucKey);
                
                throw new DataIntegrityViolationException("No unique property context exists for id: " + id);
            }
            entity.setValue1PropId(id1);
            entity.setValue2PropId(id2);
            entity.setValue3PropId(id3);
            
            entity = updatePropertyUniqueContext(entity);
            
            controlDAO.releaseSavepoint(savepoint);
            
            // cache
            propertyUniqueContextCache.put(pucKey, entity);
            
            // Done
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Updated unique property context: \n" +
                        "   ID: " + id + "\n" +
                        "   Values: " + value1 + "-" + value2 + "-" + value3);
            }
            return;
        }
        catch (Throwable e)
        {
            // Remove from cache
            propertyUniqueContextCache.remove(pucKey);
            
            controlDAO.rollbackToSavepoint(savepoint);
            throw new PropertyUniqueConstraintViolation(value1, value2, value3, e);
        }
    }
    
    /* 
     * Update property value by keys
     */
    public void updatePropertyUniqueContext(Serializable value1, Serializable value2, Serializable value3, Serializable propertyValue)
    {
        // Translate the properties.  Null values are acceptable
        Long id1 = getOrCreatePropertyValue(value1).getFirst();
        Long id2 = getOrCreatePropertyValue(value2).getFirst();
        Long id3 = getOrCreatePropertyValue(value3).getFirst();
        
        CachePucKey pucKey = getPucKey(id1, id2, id3);
        
        try
        {
            Pair<Long, Long> entityPair = getPropertyUniqueContext(value1, value2, value3);
            if (entityPair == null)
            {
                throw new DataIntegrityViolationException("No unique property context exists for values: " + value1 + "-" + value2 + "-" + value3);
            }
            
            long id = entityPair.getFirst();
            PropertyUniqueContextEntity entity = getPropertyUniqueContextById(id);
            if (entity == null)
            {
                throw new DataIntegrityViolationException("No unique property context exists for id: " + id);
            }
            
            Long propertyId = null;
            if (propertyValue != null)
            {
                propertyId = createProperty(propertyValue);
            }
            
            // Create a new property
            entity.setPropertyId(propertyId);
            
            entity = updatePropertyUniqueContext(entity);
            
            // cache
            propertyUniqueContextCache.put(pucKey, entity);
            
            // Done
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Updated unique property context: \n" +
                        "   ID: " + id + "\n" +
                        "   Property: " + propertyId);
            }
        }
        catch (DataIntegrityViolationException e)
        {
            // Remove from cache
            propertyUniqueContextCache.remove(pucKey);
            throw e;
        }
        catch (ConcurrencyFailureException e)
        {
            // Remove from cache
            propertyUniqueContextCache.remove(pucKey);
            throw e;
        }
    }

    public int deletePropertyUniqueContext(Serializable... values)
    {
        if (values.length < 1 || values.length > 3)
        {
            throw new IllegalArgumentException("Deletion of unique property sets must have 1, 2 or 3 values");
        }
        Long[] valueIds = new Long[values.length];
        for (int i = 0; i < values.length; i++)
        {
            Pair<Long, Serializable> valuePair = getPropertyValue(values[i]);
            if (valuePair == null)
            {
                // No such value, so no need to delete
                return 0;
            }
            valueIds[i] = valuePair.getFirst();
        }
        int deleted = deletePropertyUniqueContexts(valueIds);
        
        CachePucKey pucKey = getPucKey(valueIds[0], (values.length > 1 ? valueIds[1] : null), (values.length > 2 ? valueIds[2] : null));
        
        if (values.length == 3)
        {
            propertyUniqueContextCache.remove(pucKey);
        }
        else
        {
            // note: in future, if we need to support mass removal based on specific key grouping then we need to use more intelligent cache (removal)
            propertyUniqueContextCache.clear();
        }
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Deleted " + deleted + " unique property contexts: \n" +
                    "   Values: " + Arrays.toString(values) + "\n" +
                    "   IDs:    " + Arrays.toString(valueIds));
        }
        return deleted;
    }

    protected abstract PropertyUniqueContextEntity createPropertyUniqueContext(Long valueId1, Long valueId2, Long valueId3, Long propertyId);
    protected abstract PropertyUniqueContextEntity getPropertyUniqueContextById(Long id);
    protected abstract PropertyUniqueContextEntity getPropertyUniqueContextByValues(Long valueId1, Long valueId2, Long valueId3);
    protected abstract void getPropertyUniqueContextByValues(PropertyUniqueContextCallback callback, Long... valueIds);
    protected abstract PropertyUniqueContextEntity updatePropertyUniqueContext(PropertyUniqueContextEntity entity);
    protected abstract int deletePropertyUniqueContexts(Long ... valueIds);

    //================================
    // Utility methods
    //================================

    @SuppressWarnings("unchecked")
    public Serializable convertPropertyIdSearchRows(List<PropertyIdSearchRow> rows)
    {
        // Shortcut if there are no results
        if (rows.size() == 0)
        {
            return null;
        }
        /*
         * The results all share the same root property.  Pass through the results and construct all
         * instances, storing them ordered by prop_index.
         */
        Map<Long, Serializable> valuesByPropIndex = new HashMap<Long, Serializable>(7);
        TreeMap<Long, PropertyLinkEntity> linkEntitiesByPropIndex = new TreeMap<Long, PropertyLinkEntity>();
        Long rootPropId = null;                         // Keep this to ensure the root_prop_id is common
        for (PropertyIdSearchRow row : rows)
        {
            // Check that we are handling a single root property
            if (rootPropId == null)
            {
                rootPropId = row.getLinkEntity().getRootPropId();
            }
            else if (!rootPropId.equals(row.getLinkEntity().getRootPropId()))
            {
                throw new IllegalArgumentException(
                        "The root_prop_id for the property search rows must not change: \n" +
                        "   Rows: " + rows);
            }
            
            PropertyLinkEntity linkEntity = row.getLinkEntity();
            Long propIndex = linkEntity.getPropIndex();
            Long valuePropId = linkEntity.getValuePropId();
            PropertyValueEntity valueEntity = row.getValueEntity();
            // Get the value
            Serializable value;
            if (valueEntity != null)
            {
                value = propertyValueCallback.convertToValue(valueEntity);
            }
            else
            {
                // Go N+1 if the value entity was not retrieved
                value = getPropertyValueById(valuePropId);
            }
            // Keep it for later
            valuesByPropIndex.put(propIndex, value);
            linkEntitiesByPropIndex.put(propIndex, linkEntity);
        }
        
        Serializable result = null;
        // Iterate again, adding values to the collections and looking for the root property
        for (Map.Entry<Long, PropertyLinkEntity> entry : linkEntitiesByPropIndex.entrySet())
        {
            PropertyLinkEntity linkEntity = entry.getValue();
            Long propIndex = linkEntity.getPropIndex();
            Long containedIn = linkEntity.getContainedIn();
            Long keyPropId = linkEntity.getKeyPropId();
            Serializable value = valuesByPropIndex.get(propIndex);
            // Check if this is the root property
            if (propIndex.equals(containedIn))
            {
                if (result != null)
                {
                    logger.error("Found inconsistent property root data: " + linkEntity);
                    continue;
                }
                // This property is contained in itself i.e. it's the root
                result = value;
            }
            else
            {
                // Add the value to the container to which it belongs.
                // The ordering is irrelevant for some containers; but where it is important,
                // ordering given by the prop_index will ensure that values are added back
                // in the order in which the container originally iterated over them
                Serializable container = valuesByPropIndex.get(containedIn);
                if (container == null)
                {
                    logger.error("Found container ID that doesn't have a value: " + linkEntity);
                }
                else if (container instanceof Map<?, ?>)
                {
                    Map<Serializable, Serializable> map = (Map<Serializable, Serializable>) container;
                    Serializable mapKey = getPropertyValueById(keyPropId).getSecond();
                    map.put(mapKey, value);
                }
                else if (container instanceof Collection<?>)
                {
                    Collection<Serializable> collection = (Collection<Serializable>) container;
                    collection.add(value);
                }
                else
                {
                    logger.error("Found container ID that is not a map or collection: " + linkEntity);
                }
            }
        }
        // This will have put the values into the correct containers
        return result;
    }

    protected void clearCaches()
    {
        propertyClassCache.clear();
        propertyDateValueCache.clear();
        propertyStringValueCache.clear();
        propertyDoubleValueCache.clear();
        propertySerializableValueCache.clear();
        propertyCache.clear();
        propertyValueCache.clear();
    }
}