/*
 * Copyright 2017 - 2020 Acosix GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.acosix.alfresco.simplecontentstores.repo.store.file;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentLimitProvider;
import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider;
import org.alfresco.repo.content.ContentLimitProvider.SimpleFixedLimitProvider;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.copy.CopyServicePolicies.OnCopyCompletePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.acosix.alfresco.simplecontentstores.repo.store.context.ContentStoreContext;
import de.acosix.alfresco.simplecontentstores.repo.store.routing.MoveCapableCommonRoutingContentStore;

/**
 * @author Axel Faust
 */
public class SiteRoutingFileContentStore extends MoveCapableCommonRoutingContentStore<Void>
        implements OnCopyCompletePolicy, OnMoveNodePolicy
{

    private static final Logger LOGGER = LoggerFactory.getLogger(SiteRoutingFileContentStore.class);

    protected NamespaceService namespaceService;

    protected String rootAbsolutePath;

    protected Map<String, String> rootAbsolutePathsBySitePreset;

    protected Map<String, String> rootAbsolutePathsBySite;

    protected String protocol = FileContentStore.STORE_PROTOCOL;

    protected Map<String, String> protocolsBySitePreset;

    protected Map<String, String> protocolsBySite;

    protected transient Map<String, SiteAwareFileContentStore> storeByProtocol = new HashMap<>();

    protected boolean allowRandomAccess;

    protected boolean readOnly;

    protected boolean deleteEmptyDirs = true;

    protected boolean useSiteFolderInGenericDirectories;

    protected boolean moveStoresOnNodeMoveOrCopy;

    protected ContentLimitProvider contentLimitProvider = new NoLimitProvider();

    protected Map<String, ContentLimitProvider> contentLimitProviderBySitePreset;

    protected Map<String, ContentLimitProvider> contentLimitProviderBySite;

    protected String moveStoresOnNodeMoveOrCopyOverridePropertyName;

    protected transient QName moveStoresOnNodeMoveOrCopyOverridePropertyQName;

    /**
     *
     * {@inheritDoc}
     */
    @Override
    public void afterPropertiesSet()
    {
        PropertyCheck.mandatory(this, "namespaceService", this.namespaceService);

        this.afterPropertiesSet_setupDefaultStore();

        super.afterPropertiesSet();

        this.afterPropertiesSet_setupStoreData();
        this.afterPropertiesSet_setupChangePolicies();
    }

    /**
     * @param namespaceService
     *            the namespaceService to set
     */
    public void setNamespaceService(final NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }

    /**
     * @param rootAbsolutePath
     *            the rootAbsolutePath to set
     */
    public void setRootAbsolutePath(final String rootAbsolutePath)
    {
        this.rootAbsolutePath = rootAbsolutePath;
    }

    /**
     * Simple alias to {@link #setRootAbsolutePath(String)} for compatibility with previously used {@code FileContentStoreFactoryBean}
     *
     * @param rootDirectory
     *            the rootDirectory to set
     */
    public void setRootDirectory(final String rootDirectory)
    {
        this.rootAbsolutePath = rootDirectory;
    }

    /**
     * @param rootAbsolutePathsBySitePreset
     *            the rootAbsolutePathsBySitePreset to set
     */
    public void setRootAbsolutePathsBySitePreset(final Map<String, String> rootAbsolutePathsBySitePreset)
    {
        this.rootAbsolutePathsBySitePreset = rootAbsolutePathsBySitePreset;
    }

    /**
     * @param rootAbsolutePathsBySite
     *            the rootAbsolutePathsBySite to set
     */
    public void setRootAbsolutePathsBySite(final Map<String, String> rootAbsolutePathsBySite)
    {
        this.rootAbsolutePathsBySite = rootAbsolutePathsBySite;
    }

    /**
     * @param protocol
     *            the protocol to set
     */
    public void setProtocol(final String protocol)
    {
        this.protocol = protocol;
    }

    /**
     * @param protocolsBySitePreset
     *            the protocolsBySitePreset to set
     */
    public void setProtocolsBySitePreset(final Map<String, String> protocolsBySitePreset)
    {
        this.protocolsBySitePreset = protocolsBySitePreset;
    }

    /**
     * @param protocolsBySite
     *            the protocolsBySite to set
     */
    public void setProtocolsBySite(final Map<String, String> protocolsBySite)
    {
        this.protocolsBySite = protocolsBySite;
    }

    /**
     * @param allowRandomAccess
     *            the allowRandomAccess to set
     */
    public void setAllowRandomAccess(final boolean allowRandomAccess)
    {
        this.allowRandomAccess = allowRandomAccess;
    }

    /**
     * @param readOnly
     *            the readOnly to set
     */
    public void setReadOnly(final boolean readOnly)
    {
        this.readOnly = readOnly;
    }

    /**
     * @param deleteEmptyDirs
     *            the deleteEmptyDirs to set
     */
    public void setDeleteEmptyDirs(final boolean deleteEmptyDirs)
    {
        this.deleteEmptyDirs = deleteEmptyDirs;
    }

    /**
     * @param useSiteFolderInGenericDirectories
     *            the useSiteFolderInGenericDirectories to set
     */
    public void setUseSiteFolderInGenericDirectories(final boolean useSiteFolderInGenericDirectories)
    {
        this.useSiteFolderInGenericDirectories = useSiteFolderInGenericDirectories;
    }

    /**
     * @param contentLimitProvider
     *            the contentLimitProvider to set
     *
     */
    public void setContentLimitProvider(final ContentLimitProvider contentLimitProvider)
    {
        this.contentLimitProvider = contentLimitProvider;
    }

    /**
     *
     * @param limit
     *            the fixed content limit to set
     */
    public void setFixedLimit(final long limit)
    {
        if (limit < 0 && limit != ContentLimitProvider.NO_LIMIT)
        {
            throw new IllegalArgumentException("fixedLimit must be non-negative");
        }
        this.setContentLimitProvider(new SimpleFixedLimitProvider(limit));
    }

    /**
     * @param contentLimitProviderBySitePreset
     *            the contentLimitProviderBySitePreset to set
     */
    public void setContentLimitProviderBySitePreset(final Map<String, ContentLimitProvider> contentLimitProviderBySitePreset)
    {
        this.contentLimitProviderBySitePreset = contentLimitProviderBySitePreset;
    }

    /**
     *
     * @param limits
     *            the fixed limits to set
     */
    public void setFixedLimitBySitePreset(final Map<String, Long> limits)
    {
        ParameterCheck.mandatory("limits", limits);

        if (this.contentLimitProviderBySitePreset == null)
        {
            this.contentLimitProviderBySitePreset = new HashMap<>();
        }

        for (final Entry<String, Long> limitEntry : limits.entrySet())
        {
            final long limit = limitEntry.getValue().longValue();
            if (limit < 0 && limit != ContentLimitProvider.NO_LIMIT)
            {
                throw new IllegalArgumentException("fixedLimit must be non-negative");
            }
            this.contentLimitProviderBySitePreset.put(limitEntry.getKey(), new SimpleFixedLimitProvider(limit));
        }
    }

    /**
     * @param contentLimitProviderBySite
     *            the contentLimitProviderBySite to set
     */
    public void setContentLimitProviderBySite(final Map<String, ContentLimitProvider> contentLimitProviderBySite)
    {
        this.contentLimitProviderBySite = contentLimitProviderBySite;
    }

    /**
     *
     * @param limits
     *            the fixed limits to set
     */
    public void setFixedLimitBySite(final Map<String, Long> limits)
    {
        ParameterCheck.mandatory("limits", limits);

        if (this.contentLimitProviderBySite == null)
        {
            this.contentLimitProviderBySite = new HashMap<>();
        }

        for (final Entry<String, Long> limitEntry : limits.entrySet())
        {
            final long limit = limitEntry.getValue().longValue();
            if (limit < 0 && limit != ContentLimitProvider.NO_LIMIT)
            {
                throw new IllegalArgumentException("fixedLimit must be non-negative");
            }
            this.contentLimitProviderBySite.put(limitEntry.getKey(), new SimpleFixedLimitProvider(limit));
        }
    }

    /**
     * @param moveStoresOnNodeMoveOrCopy
     *            the moveStoresOnNodeMoveOrCopy to set
     */
    public void setMoveStoresOnNodeMoveOrCopy(final boolean moveStoresOnNodeMoveOrCopy)
    {
        this.moveStoresOnNodeMoveOrCopy = moveStoresOnNodeMoveOrCopy;
    }

    /**
     * @param moveStoresOnNodeMoveOrCopyOverridePropertyName
     *            the moveStoresOnNodeMoveOrCopyOverridePropertyName to set
     */
    public void setMoveStoresOnNodeMoveOrCopyOverridePropertyName(final String moveStoresOnNodeMoveOrCopyOverridePropertyName)
    {
        this.moveStoresOnNodeMoveOrCopyOverridePropertyName = moveStoresOnNodeMoveOrCopyOverridePropertyName;
    }

    /**
     *
     * {@inheritDoc}
     */
    @Override
    public boolean isContentUrlSupported(final String contentUrl)
    {
        // optimisation: check the likely candidate store based on context first
        final ContentStore storeForCurrentContext = this.selectStoreForCurrentContext();

        boolean supported = false;
        if (storeForCurrentContext != null)
        {
            LOGGER.debug("Preferentially using store for current context to check support for content URL {}", contentUrl);
            supported = storeForCurrentContext.isContentUrlSupported(contentUrl);
        }

        if (!supported)
        {
            LOGGER.debug("Delegating to super implementation to check support for content URL {}", contentUrl);
            supported = super.isContentUrlSupported(contentUrl);
        }
        return supported;
    }

    /**
     *
     * {@inheritDoc}
     */
    @Override
    public boolean isWriteSupported()
    {
        // optimisation: check the likely candidate store based on context first
        final ContentStore storeForCurrentContext = this.selectStoreForCurrentContext();

        boolean supported = false;
        if (storeForCurrentContext != null)
        {
            LOGGER.debug("Preferentially using store for current context to check write suport");
            supported = storeForCurrentContext.isWriteSupported();
        }

        if (!supported)
        {
            LOGGER.debug("Delegating to super implementation to check write support");
            supported = super.isWriteSupported();
        }
        return supported;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onMoveNode(final ChildAssociationRef oldChildAssocRef, final ChildAssociationRef newChildAssocRef)
    {
        // only act on active nodes which can actually be in a site
        // only act on active nodes which can actually be in a site
        final NodeRef movedNode = oldChildAssocRef.getChildRef();
        final NodeRef oldParent = oldChildAssocRef.getParentRef();
        final NodeRef newParent = newChildAssocRef.getParentRef();
        if (StoreRef.STORE_REF_WORKSPACE_SPACESSTORE.equals(movedNode.getStoreRef()) && !EqualsHelper.nullSafeEquals(oldParent, newParent))
        {
            LOGGER.debug("Processing onMoveNode for {} from {} to {}", movedNode, oldChildAssocRef, newChildAssocRef);

            // check for actual move-relevant site move
            final Boolean moveRelevant = AuthenticationUtil.runAsSystem(() -> {
                final NodeRef sourceSite = this.resolveSiteForNode(oldParent);
                final NodeRef targetSite = this.resolveSiteForNode(newParent);

                final SiteAwareFileContentStore sourceStore = this.resolveStoreForSite(sourceSite);
                final SiteAwareFileContentStore targetStore = this.resolveStoreForSite(targetSite);

                boolean moveRelevantB = sourceStore != targetStore;
                if (!moveRelevantB && !EqualsHelper.nullSafeEquals(sourceSite, targetSite)
                        && targetStore.isUseSiteFolderInGenericDirectories())
                {
                    moveRelevantB = true;
                }
                return Boolean.valueOf(moveRelevantB);
            });

            if (Boolean.TRUE.equals(moveRelevant))
            {
                LOGGER.debug("Node {} was moved to a location for which content should be stored in a different store", movedNode);
                this.checkAndProcessContentPropertiesMove(movedNode);
            }
            else
            {
                LOGGER.debug("Node {} was not moved into a location for which content should be stored in a different store", movedNode);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCopyComplete(final QName classRef, final NodeRef sourceNodeRef, final NodeRef targetNodeRef, final boolean copyToNewNode,
            final Map<NodeRef, NodeRef> copyMap)
    {
        // only act on active nodes which can actually be in a site
        if (StoreRef.STORE_REF_WORKSPACE_SPACESSTORE.equals(targetNodeRef.getStoreRef()))
        {
            LOGGER.debug("Processing onCopyComplete for copy from {} to {}", sourceNodeRef, targetNodeRef);

            // check for actual move-relevant site copy
            final Boolean moveRelevant = AuthenticationUtil.runAsSystem(() -> {
                final NodeRef sourceSite = this.resolveSiteForNode(sourceNodeRef);
                final NodeRef targetSite = this.resolveSiteForNode(targetNodeRef);

                final SiteAwareFileContentStore sourceStore = this.resolveStoreForSite(sourceSite);
                final SiteAwareFileContentStore targetStore = this.resolveStoreForSite(targetSite);

                boolean moveRelevantB = sourceStore != targetStore;
                if (!moveRelevantB && !EqualsHelper.nullSafeEquals(sourceSite, targetSite)
                        && targetStore.isUseSiteFolderInGenericDirectories())
                {
                    moveRelevantB = true;
                }
                return Boolean.valueOf(moveRelevantB);
            });

            if (Boolean.TRUE.equals(moveRelevant))
            {
                LOGGER.debug("Node {} was copied into a location for which content should be stored in a different store", targetNodeRef);
                this.checkAndProcessContentPropertiesMove(targetNodeRef);
            }
            else
            {
                LOGGER.debug("Node {} was not copied into a location for which content should be stored in a different store",
                        targetNodeRef);
            }
        }
    }

    protected void checkAndProcessContentPropertiesMove(final NodeRef affectedNode)
    {
        this.checkAndProcessContentPropertiesMove(affectedNode, this.moveStoresOnNodeMoveOrCopy,
                this.moveStoresOnNodeMoveOrCopyOverridePropertyQName, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ContentStore selectStore(final String contentUrl, final boolean mustExist)
    {
        // optimisation: check the likely candidate store based on context first
        final ContentStore storeForCurrentContext = this.selectStoreForCurrentContext();

        ContentStore store = null;
        if (storeForCurrentContext != null)
        {
            LOGGER.debug(
                    "Preferentially testing store for current context to select store for read of content URL {} with mustExist flag of {}",
                    contentUrl, mustExist);
            if (!mustExist || (storeForCurrentContext.isContentUrlSupported(contentUrl) && storeForCurrentContext.exists(contentUrl)))
            {
                store = storeForCurrentContext;
            }
        }

        if (store == null)
        {
            LOGGER.debug("Delegating to super implementation to select store for read of content URL {} with mustExist flag of {}",
                    contentUrl, mustExist);
            store = super.selectStore(contentUrl, mustExist);
        }
        return store;
    }

    /**
     *
     * {@inheritDoc}
     */
    @Override
    protected ContentStore selectStoreForContentDataMove(final NodeRef nodeRef, final QName propertyQName, final ContentData contentData,
            final Void customData)
    {
        final ContentStore writeStore = this.selectStoreForCurrentContext();
        return writeStore;
    }

    /**
     *
     * {@inheritDoc}
     */
    @Override
    protected ContentStore selectWriteStoreFromRoutes(final ContentContext ctx)
    {
        final ContentStore writeStore = this.selectStoreForCurrentContext();
        return writeStore;
    }

    protected ContentStore selectStoreForCurrentContext()
    {
        final String site = DefaultTypeConverter.INSTANCE.convert(String.class,
                ContentStoreContext.getContextAttribute(ContentStoreContext.DEFAULT_ATTRIBUTE_SITE));
        final String sitePreset = DefaultTypeConverter.INSTANCE.convert(String.class,
                ContentStoreContext.getContextAttribute(ContentStoreContext.DEFAULT_ATTRIBUTE_SITE_PRESET));

        return this.resolveStoreForSite(site, sitePreset);
    }

    /**
     * Resolves the content store to use for a particular site.
     *
     * @param siteNode
     *            the node representing the site - may be {@code null}
     * @return the content store to use for the site - never {@code null}
     */
    protected SiteAwareFileContentStore resolveStoreForSite(final NodeRef siteNode)
    {
        String site = null;
        String sitePreset = null;

        if (siteNode != null)
        {
            final Map<QName, Serializable> properties = this.nodeService.getProperties(siteNode);
            site = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_NAME));
            sitePreset = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(SiteModel.PROP_SITE_PRESET));
        }

        return this.resolveStoreForSite(site, sitePreset);
    }

    /**
     * Resolves the content store to use for a particular site.
     *
     * @param site
     *            the short name of the site
     * @param sitePreset
     *            the preset of the site
     * @return the content store to use for the site - never {@code null}
     */
    protected SiteAwareFileContentStore resolveStoreForSite(final String site, final String sitePreset)
    {
        LOGGER.debug("Resolving store for site {} and preset {}", site, sitePreset);

        final String protocol;
        if (this.protocolsBySite != null && site != null && this.protocolsBySite.containsKey(site))
        {
            protocol = this.protocolsBySite.get(site);
        }
        else if (this.protocolsBySitePreset != null && sitePreset != null && this.protocolsBySitePreset.containsKey(sitePreset))
        {
            protocol = this.protocolsBySitePreset.get(sitePreset);
        }
        else
        {
            protocol = this.protocol;
        }

        final SiteAwareFileContentStore targetStore = this.storeByProtocol.get(protocol);
        LOGGER.debug("Resolved store {}", targetStore);
        return targetStore;
    }

    /**
     * This internal method only exists to avoid the circular dependency we would create when requiring the {@link SiteService} as a
     * dependency for {@link SiteService#getSite(NodeRef) resolving the site of a node}.
     *
     * @param node
     *            the node for which to resolve the site
     * @return the node reference for the site, or {@code null} if the node is not contained in a site via a graph of primary parent
     *         associations
     */
    protected NodeRef resolveSiteForNode(final NodeRef node)
    {
        NodeRef site = null;
        NodeRef curParent = node;
        while (curParent != null)
        {
            final QName curParentType = this.nodeService.getType(curParent);
            if (this.dictionaryService.isSubClass(curParentType, SiteModel.TYPE_SITE))
            {
                site = curParent;
                break;
            }
            curParent = this.nodeService.getPrimaryParent(curParent).getParentRef();
        }
        return site;
    }

    protected void afterPropertiesSet_setupDefaultStore()
    {
        PropertyCheck.mandatory(this, "rootAbsolutePath", this.rootAbsolutePath);
        PropertyCheck.mandatory(this, "protocol", this.protocol);

        if (this.allStores == null)
        {
            this.allStores = new ArrayList<>();
        }

        final SiteAwareFileContentStore defaultFileContentStore = new SiteAwareFileContentStore();
        defaultFileContentStore.setProtocol(this.protocol);
        defaultFileContentStore.setRootAbsolutePath(this.rootAbsolutePath);
        defaultFileContentStore.setApplicationContext(this.applicationContext);
        defaultFileContentStore.setContentLimitProvider(this.contentLimitProvider);
        defaultFileContentStore.setAllowRandomAccess(this.allowRandomAccess);
        defaultFileContentStore.setDeleteEmptyDirs(this.deleteEmptyDirs);
        defaultFileContentStore.setReadOnly(this.readOnly);
        defaultFileContentStore.setUseSiteFolderInGenericDirectories(this.useSiteFolderInGenericDirectories);

        this.storeByProtocol.put(this.protocol, defaultFileContentStore);
        this.allStores.add(defaultFileContentStore);
        this.fallbackStore = defaultFileContentStore;

        defaultFileContentStore.afterPropertiesSet();
    }

    protected void afterPropertiesSet_setupStoreData()
    {
        if (this.rootAbsolutePathsBySite != null && !this.rootAbsolutePathsBySite.isEmpty())
        {
            PropertyCheck.mandatory(this, "protocolBySite", this.protocolsBySite);

            for (final Entry<String, String> entry : this.rootAbsolutePathsBySite.entrySet())
            {
                final String site = entry.getKey();
                final String protocol = this.protocolsBySite.get(site);
                PropertyCheck.mandatory(this, "protocolBySite." + site, protocol);

                if (this.storeByProtocol.containsKey(protocol))
                {
                    throw new ContentIOException("Failed to set up site aware content store - duplicate protocol: " + protocol, null);
                }

                final SiteAwareFileContentStore siteAwareFileContentStore = new SiteAwareFileContentStore();
                siteAwareFileContentStore.setProtocol(protocol);
                siteAwareFileContentStore.setRootAbsolutePath(entry.getValue());
                siteAwareFileContentStore.setApplicationContext(this.applicationContext);

                ContentLimitProvider contentLimitProvider = null;
                if (this.contentLimitProviderBySite != null)
                {
                    contentLimitProvider = this.contentLimitProviderBySite.get(site);
                }
                if (contentLimitProvider == null)
                {
                    contentLimitProvider = this.contentLimitProvider;
                }
                siteAwareFileContentStore.setContentLimitProvider(contentLimitProvider);

                siteAwareFileContentStore.setAllowRandomAccess(this.allowRandomAccess);
                siteAwareFileContentStore.setDeleteEmptyDirs(this.deleteEmptyDirs);
                siteAwareFileContentStore.setReadOnly(this.readOnly);
                siteAwareFileContentStore.setExtendedEventParameters(Collections.<String, Serializable> singletonMap("Site", site));

                this.storeByProtocol.put(protocol, siteAwareFileContentStore);
                this.allStores.add(siteAwareFileContentStore);

                siteAwareFileContentStore.afterPropertiesSet();
            }

            if (!this.rootAbsolutePathsBySite.keySet().containsAll(this.protocolsBySite.keySet()))
            {
                throw new IllegalStateException(
                        "Invalid store configuration: 'protocolsBySite' specifies protocols for more sites than 'rootAbsolutePathsBySite' specifies storage locations");
            }
        }
        else if (this.protocolsBySite != null && !this.protocolsBySite.isEmpty())
        {
            throw new IllegalStateException(
                    "Invalid store configuration: 'protocolsBySite' is set without corresponding 'rootAbsolutePathsBySite'");
        }

        if (this.rootAbsolutePathsBySitePreset != null && !this.rootAbsolutePathsBySitePreset.isEmpty())
        {
            PropertyCheck.mandatory(this, "protocolBySitePreset", this.protocolsBySitePreset);

            for (final Entry<String, String> entry : this.rootAbsolutePathsBySitePreset.entrySet())
            {
                final String sitePreset = entry.getKey();
                final String protocol = this.protocolsBySitePreset.get(sitePreset);
                PropertyCheck.mandatory(this, "protocolBySitePreset." + sitePreset, protocol);

                if (this.storeByProtocol.containsKey(protocol))
                {
                    throw new ContentIOException("Failed to set up site aware content store - duplicate protocol: " + protocol, null);
                }

                final SiteAwareFileContentStore siteAwareFileContentStore = new SiteAwareFileContentStore();
                siteAwareFileContentStore.setProtocol(protocol);
                siteAwareFileContentStore.setRootAbsolutePath(entry.getValue());
                siteAwareFileContentStore.setApplicationContext(this.applicationContext);

                ContentLimitProvider contentLimitProvider = null;
                if (this.contentLimitProviderBySitePreset != null)
                {
                    contentLimitProvider = this.contentLimitProviderBySitePreset.get(sitePreset);
                }
                if (contentLimitProvider == null)
                {
                    contentLimitProvider = this.contentLimitProvider;
                }
                siteAwareFileContentStore.setContentLimitProvider(contentLimitProvider);

                siteAwareFileContentStore.setAllowRandomAccess(this.allowRandomAccess);
                siteAwareFileContentStore.setDeleteEmptyDirs(this.deleteEmptyDirs);
                siteAwareFileContentStore.setReadOnly(this.readOnly);
                siteAwareFileContentStore.setUseSiteFolderInGenericDirectories(this.useSiteFolderInGenericDirectories);
                siteAwareFileContentStore
                        .setExtendedEventParameters(Collections.<String, Serializable> singletonMap("SitePreset", sitePreset));

                this.storeByProtocol.put(protocol, siteAwareFileContentStore);
                this.allStores.add(siteAwareFileContentStore);

                siteAwareFileContentStore.afterPropertiesSet();
            }

            if (!this.rootAbsolutePathsBySitePreset.keySet().containsAll(this.protocolsBySitePreset.keySet()))
            {
                throw new IllegalStateException(
                        "Invalid store configuration: 'protocolsBySitePreset' specifies protocols for more sites than 'rootAbsolutePathsBySitePreset' specifies storage locations");
            }
        }
        else if (this.protocolsBySitePreset != null && !this.protocolsBySitePreset.isEmpty())
        {
            throw new IllegalStateException(
                    "Invalid store configuration: 'protocolsBySitePreset' is set without corresponding 'rootAbsolutePathsBySitePreset'");
        }
    }

    protected void afterPropertiesSet_setupChangePolicies()
    {
        if (this.moveStoresOnNodeMoveOrCopyOverridePropertyName != null)
        {
            this.moveStoresOnNodeMoveOrCopyOverridePropertyQName = QName.resolveToQName(this.namespaceService,
                    this.moveStoresOnNodeMoveOrCopyOverridePropertyName);
            PropertyCheck.mandatory(this, "moveStoresOnNodeMoveOrCopyOverridePropertyQName",
                    this.moveStoresOnNodeMoveOrCopyOverridePropertyQName);

            final PropertyDefinition moveStoresOnChangeOptionPropertyDefinition = this.dictionaryService
                    .getProperty(this.moveStoresOnNodeMoveOrCopyOverridePropertyQName);
            if (moveStoresOnChangeOptionPropertyDefinition == null
                    || !DataTypeDefinition.BOOLEAN.equals(moveStoresOnChangeOptionPropertyDefinition.getDataType().getName())
                    || moveStoresOnChangeOptionPropertyDefinition.isMultiValued())
            {
                throw new IllegalStateException(this.moveStoresOnNodeMoveOrCopyOverridePropertyName
                        + " is not a valid content model property of type single-valued d:boolean");
            }
        }

        if (this.moveStoresOnNodeMoveOrCopy || this.moveStoresOnNodeMoveOrCopyOverridePropertyQName != null)
        {
            this.policyComponent.bindClassBehaviour(OnCopyCompletePolicy.QNAME, ContentModel.TYPE_BASE,
                    new JavaBehaviour(this, "onCopyComplete", NotificationFrequency.EVERY_EVENT));
            this.policyComponent.bindClassBehaviour(OnMoveNodePolicy.QNAME, ContentModel.TYPE_BASE,
                    new JavaBehaviour(this, "onMoveNode", NotificationFrequency.EVERY_EVENT));
        }
    }
}