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

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.sync.events.types.ContentEvent;
import org.alfresco.sync.events.types.ContentEventImpl;
import org.alfresco.sync.events.types.ContentReadRangeEvent;
import org.alfresco.sync.events.types.Event;
import org.alfresco.model.ContentModel;
import org.alfresco.service.cmr.activities.ActivityInfo;
import org.alfresco.opencmis.dictionary.CMISActionEvaluator;
import org.alfresco.opencmis.dictionary.CMISAllowedActionEnum;
import org.alfresco.opencmis.dictionary.CMISDictionaryService;
import org.alfresco.opencmis.dictionary.CMISNodeInfo;
import org.alfresco.opencmis.dictionary.CMISObjectVariant;
import org.alfresco.opencmis.dictionary.CMISPropertyAccessor;
import org.alfresco.opencmis.dictionary.DocumentTypeDefinitionWrapper;
import org.alfresco.opencmis.dictionary.FolderTypeDefintionWrapper;
import org.alfresco.opencmis.dictionary.ItemTypeDefinitionWrapper;
import org.alfresco.opencmis.dictionary.PropertyDefinitionWrapper;
import org.alfresco.opencmis.dictionary.TypeDefinitionWrapper;
import org.alfresco.opencmis.search.CMISQueryOptions;
import org.alfresco.opencmis.search.CMISQueryOptions.CMISQueryMode;
import org.alfresco.opencmis.search.CMISQueryService;
import org.alfresco.opencmis.search.CMISResultSet;
import org.alfresco.opencmis.search.CMISResultSetColumn;
import org.alfresco.opencmis.search.CMISResultSetRow;
import org.alfresco.sync.repo.Client;
import org.alfresco.sync.repo.Client.ClientType;
import org.alfresco.repo.action.executer.ContentMetadataExtracter;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.coci.CheckOutCheckInServiceImpl;
import org.alfresco.sync.repo.events.EventPreparator;
import org.alfresco.sync.repo.events.EventPublisher;
import org.alfresco.repo.model.filefolder.GetChildrenCannedQuery;
import org.alfresco.repo.model.filefolder.HiddenAspect;
import org.alfresco.repo.model.filefolder.HiddenAspect.Visibility;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.security.permissions.PermissionReference;
import org.alfresco.repo.security.permissions.impl.AccessPermissionImpl;
import org.alfresco.repo.security.permissions.impl.ModelDAO;
import org.alfresco.repo.tenant.TenantAdminService;
import org.alfresco.repo.tenant.TenantDeployer;
import org.alfresco.repo.thumbnail.ThumbnailDefinition;
import org.alfresco.repo.thumbnail.ThumbnailHelper;
import org.alfresco.repo.thumbnail.ThumbnailRegistry;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.version.VersionBaseModel;
import org.alfresco.repo.version.VersionModel;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.InvalidAspectException;
import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.Path.ChildAssocElement;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.cmr.thumbnail.ThumbnailService;
import org.alfresco.service.cmr.version.VersionHistory;
import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.cmr.version.VersionType;
import org.alfresco.service.descriptor.Descriptor;
import org.alfresco.service.descriptor.DescriptorService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.FileFilterMode;
import org.alfresco.util.Pair;
import org.alfresco.util.TempFileProvider;
import org.apache.chemistry.opencmis.commons.BasicPermissions;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.Ace;
import org.apache.chemistry.opencmis.commons.data.Acl;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.data.CmisExtensionElement;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.ObjectList;
import org.apache.chemistry.opencmis.commons.data.PermissionMapping;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.PropertyData;
import org.apache.chemistry.opencmis.commons.data.PropertyId;
import org.apache.chemistry.opencmis.commons.data.PropertyString;
import org.apache.chemistry.opencmis.commons.data.RenditionData;
import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
import org.apache.chemistry.opencmis.commons.definitions.DocumentTypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PermissionDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
import org.apache.chemistry.opencmis.commons.enums.Action;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.CapabilityAcl;
import org.apache.chemistry.opencmis.commons.enums.CapabilityChanges;
import org.apache.chemistry.opencmis.commons.enums.CapabilityContentStreamUpdates;
import org.apache.chemistry.opencmis.commons.enums.CapabilityJoin;
import org.apache.chemistry.opencmis.commons.enums.CapabilityQuery;
import org.apache.chemistry.opencmis.commons.enums.CapabilityRenditions;
import org.apache.chemistry.opencmis.commons.enums.Cardinality;
import org.apache.chemistry.opencmis.commons.enums.ChangeType;
import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
import org.apache.chemistry.opencmis.commons.enums.ContentStreamAllowed;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.PropertyType;
import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
import org.apache.chemistry.opencmis.commons.enums.SupportedPermissions;
import org.apache.chemistry.opencmis.commons.enums.Updatability;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisStreamNotSupportedException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AbstractPropertyData;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlEntryImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlPrincipalDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AclCapabilitiesDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ChangeEventInfoDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.CmisExtensionElementImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PermissionDefinitionDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PermissionMappingDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PolicyIdListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyBooleanImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDateTimeImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyHtmlImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIntegerImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyUriImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryCapabilitiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryInfoImpl;
import org.apache.chemistry.opencmis.commons.server.CallContext;
import org.apache.chemistry.opencmis.commons.server.CmisService;
import org.apache.chemistry.opencmis.commons.spi.Holder;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.util.StringUtils;

import static java.util.Collections.singletonList;

/**
 * Bridge connecting Alfresco and OpenCMIS.
 * <p/>
 * This class  provides many of the typical services that the {@link CmisService} implementation
 * will need to use Alfresco.
 *
 * @author florian.mueller
 * @author Derek Hulley
 * @author steveglover
 */
public class CMISConnector implements ApplicationContextAware, ApplicationListener<ApplicationContextEvent>, TenantDeployer
{
    private static Log logger = LogFactory.getLog(CMISConnector.class);

    // mappings from cmis property names to their Alfresco property name counterparts (used by getChildren)
    private static Map<String, QName> SORT_PROPERTY_MAPPINGS = new HashMap<String, QName>();

    static
    {
    	SORT_PROPERTY_MAPPINGS.put(PropertyIds.LAST_MODIFICATION_DATE, ContentModel.PROP_MODIFIED);
    	SORT_PROPERTY_MAPPINGS.put(PropertyIds.CREATION_DATE, ContentModel.PROP_CREATED);
    }

    public static final char ID_SEPERATOR = ';';
    public static final char REPLACEMENT_CHAR = '_';
    public static final String ASSOC_ID_PREFIX = "assoc:";
    public static final String PWC_VERSION_LABEL = "pwc";
    public static final String UNVERSIONED_VERSION_LABEL = "1.0";

    public static final String RENDITION_NONE = "cmis:none";

    public static final String CMIS_CHANGELOG_AUDIT_APPLICATION = "CMISChangeLog";

    public static final String ALFRESCO_EXTENSION_NAMESPACE = "http://www.alfresco.org";
    public static final String CMIS_NAMESPACE = "http://docs.oasis-open.org/ns/cmis/core/200908/";
    public static final String ASPECTS = "aspects";
    public static final String SET_ASPECTS = "setAspects";
    public static final String APPLIED_ASPECTS = "appliedAspects";
    public static final String ASPECTS_TO_ADD = "aspectsToAdd";
    public static final String ASPECTS_TO_REMOVE = "aspectsToRemove";
    public static final String PROPERTIES = "properties";

    private static final BigInteger TYPES_DEFAULT_MAX_ITEMS = BigInteger.valueOf(50);
    private static final BigInteger TYPES_DEFAULT_DEPTH = BigInteger.valueOf(-1);
    private static final BigInteger OBJECTS_DEFAULT_MAX_ITEMS = BigInteger.valueOf(200);
    private static final BigInteger OBJECTS_DEFAULT_DEPTH = BigInteger.valueOf(10);
    private static final int CONTENT_CHANGES_DEFAULT_MAX_ITEMS = 10000;

    private static final String QUERY_NAME_OBJECT_ID = "cmis:objectId";
    private static final String QUERY_NAME_OBJECT_TYPE_ID = "cmis:objectTypeId";
    private static final String QUERY_NAME_BASE_TYPE_ID = "cmis:baseTypeId";

    private static final String CMIS_USER = "cmis:user";

	private static final BigInteger maxInt = BigInteger.valueOf(Integer.MAX_VALUE);
	private static final BigInteger minInt = BigInteger.valueOf(Integer.MIN_VALUE);
	private static final BigInteger maxLong  = BigInteger.valueOf(Long.MAX_VALUE);
	private static final BigInteger minLong  = BigInteger.valueOf(Long.MIN_VALUE);

    // lifecycle
    private ProcessorLifecycle lifecycle = new ProcessorLifecycle();

    // Alfresco objects
    private DescriptorService descriptorService;
    private NodeService nodeService;
    private VersionService versionService;
    private CheckOutCheckInService checkOutCheckInService;
    private LockService lockService;
    private ContentService contentService;
    @Deprecated
    private RenditionService renditionService;
    private FileFolderService fileFolderService;
    private TenantAdminService tenantAdminService;
    private TransactionService transactionService;
    private AuthenticationService authenticationService;
    private PermissionService permissionService;
    private ModelDAO permissionModelDao;
    private CMISDictionaryService cmisDictionaryService;
    private CMISDictionaryService cmisDictionaryService11;
    private CMISQueryService cmisQueryService;
    private CMISQueryService cmisQueryService11;
    private MimetypeService mimetypeService;
    private AuditService auditService;
    private NamespaceService namespaceService;
    private SearchService searchService;
    private DictionaryService dictionaryService;
    private SiteService siteService;
    private ActionService actionService;
    @Deprecated
    private ThumbnailService thumbnailService;
    private ServiceRegistry serviceRegistry;
    private EventPublisher eventPublisher;

    private CmisActivityPoster activityPoster;

    private BehaviourFilter behaviourFilter;

    private HiddenAspect hiddenAspect;

    private StoreRef storeRef;
    private String rootPath;
    private Map<String, List<String>> kindToRenditionNames;

    // note: cache is tenant-aware (if using TransctionalCache impl)

    private SimpleCache<String, Object> singletonCache; // eg. for cmisRootNodeRef, cmisRenditionMapping
    private final String KEY_CMIS_ROOT_NODEREF = "key.cmisRoot.noderef";
    private final String KEY_CMIS_RENDITION_MAPPING_NODEREF = "key.cmisRenditionMapping.noderef";

    private String proxyUser;
    private boolean openHttpSession = false;

    // OpenCMIS objects
    private BigInteger typesDefaultMaxItems = TYPES_DEFAULT_MAX_ITEMS;
    private BigInteger typesDefaultDepth = TYPES_DEFAULT_DEPTH;
    private BigInteger objectsDefaultMaxItems = OBJECTS_DEFAULT_MAX_ITEMS;
    private BigInteger objectsDefaultDepth = OBJECTS_DEFAULT_DEPTH;
    private int contentChangesDefaultMaxItems = CONTENT_CHANGES_DEFAULT_MAX_ITEMS;

    private List<PermissionDefinition> repositoryPermissions;
    private Map<String, PermissionMapping> permissionMappings;

    private ObjectFilter objectFilter;

    // Bulk update properties
    private int bulkMaxItems = 1000;
    private int bulkBatchSize = 20;
    private int bulkWorkerThreads = 2;

    // --------------------------------------------------------------
    // Configuration
    // --------------------------------------------------------------

    public void setObjectFilter(ObjectFilter objectFilter)
    {
		this.objectFilter = objectFilter;
	}

	/**
     * Sets the root store.
     *
     * @param store
     *            store_type://store_id
     */
    public void setStore(String store)
    {
        this.storeRef = new StoreRef(store);
    }

    public void setSiteService(SiteService siteService)
    {
		this.siteService = siteService;
	}

	public void setActivityPoster(CmisActivityPoster activityPoster)
    {
		this.activityPoster = activityPoster;
	}

	public CmisActivityPoster getActivityPoster()
	{
		return activityPoster;
	}

	public void setHiddenAspect(HiddenAspect hiddenAspect)
    {
		this.hiddenAspect = hiddenAspect;
	}

    public boolean isHidden(NodeRef nodeRef)
    {
        final FileFilterMode.Client client = FileFilterMode.getClient();
    	return (hiddenAspect.getVisibility(client, nodeRef) == Visibility.NotVisible);
    }

    /**
     * Sets the root path.
     *
     * @param path
     *            path within default store
     */
    public void setRootPath(String path)
    {
        rootPath = path;
    }

    public BigInteger getTypesDefaultMaxItems()
    {
        return typesDefaultMaxItems;
    }

    public void setTypesDefaultMaxItems(BigInteger typesDefaultMaxItems)
    {
        this.typesDefaultMaxItems = typesDefaultMaxItems;
    }

    public BigInteger getTypesDefaultDepth()
    {
        return typesDefaultDepth;
    }

    public void setTypesDefaultDepth(BigInteger typesDefaultDepth)
    {
        this.typesDefaultDepth = typesDefaultDepth;
    }

    public BigInteger getObjectsDefaultMaxItems()
    {
        return objectsDefaultMaxItems;
    }

    public void setObjectsDefaultMaxItems(BigInteger objectsDefaultMaxItems)
    {
        this.objectsDefaultMaxItems = objectsDefaultMaxItems;
    }

    public BigInteger getObjectsDefaultDepth()
    {
        return objectsDefaultDepth;
    }

    public void setObjectsDefaultDepth(BigInteger objectsDefaultDepth)
    {
        this.objectsDefaultDepth = objectsDefaultDepth;
    }

    /**
     * Set the default number of content changes to return if nothing is specified
     */
    public void setContentChangesDefaultMaxItems(int contentChangesDefaultMaxItems)
    {
        if (contentChangesDefaultMaxItems < 1)
        {
            throw new IllegalArgumentException("The default maximum number of content changes to retrieve must be greater than zero.");
        }
        else if (contentChangesDefaultMaxItems == Integer.MAX_VALUE)
        {
            throw new IllegalArgumentException("The server cannot return " + Integer.MAX_VALUE + " content changes in a request!");
        }
        this.contentChangesDefaultMaxItems = contentChangesDefaultMaxItems;
    }

    /**
     * Set rendition kind mapping.
     */
    public void setRenditionKindMapping(Map<String, List<String>> renditionKinds)
    {
        this.kindToRenditionNames = renditionKinds;
    }

    public void setOpenHttpSession(boolean openHttpSession)
    {
        this.openHttpSession = openHttpSession;
    }

    public boolean openHttpSession()
    {
        return openHttpSession;
    }

    /**
     *
     *
     * @deprecated The thumbnails code is being moved out of the codebase and replaced by the new async RenditionService2 or other external libraries.
     */
    @Deprecated
    public void setThumbnailService(ThumbnailService thumbnailService)
    {
		this.thumbnailService = thumbnailService;
	}

	public void setServiceRegistry(ServiceRegistry serviceRegistry)
	{
		this.serviceRegistry = serviceRegistry;
	}

    /**
     * Return the service registry
     */
    public final ServiceRegistry getServiceRegistry()
    {
    	return this.serviceRegistry;
    }

	/**
     * Sets the descriptor service.
     */
    public void setDescriptorService(DescriptorService descriptorService)
    {
        this.descriptorService = descriptorService;
    }

    public DescriptorService getDescriptorService()
    {
        return descriptorService;
    }

    public void setBehaviourFilter(BehaviourFilter behaviourFilter)
    {
        this.behaviourFilter = behaviourFilter;
    }

    /**
     * Sets the node service.
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    public NodeService getNodeService()
    {
        return nodeService;
    }

	public void setActionService(ActionService actionService)
	{
		this.actionService = actionService;
	}

	/**
     * Sets the version service.
     */
    public void setVersionService(VersionService versionService)
    {
        this.versionService = versionService;
    }

    public VersionService getVersionService()
    {
        return versionService;
    }

    /**
     * Sets the checkOut/checkIn service.
     */
    public void setCheckOutCheckInService(CheckOutCheckInService checkOutCheckInService)
    {
        this.checkOutCheckInService = checkOutCheckInService;
    }

    public CheckOutCheckInService getCheckOutCheckInService()
    {
        return checkOutCheckInService;
    }

    /**
     * Sets the lock service.
     */
    public LockService getLockService()
    {
        return lockService;
    }

    public void setLockService(LockService lockService)
    {
        this.lockService = lockService;
    }

    /**
     * Sets the content service.
     */
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }

    public ContentService getContentService()
    {
        return contentService;
    }

    /**
     * Sets the event publisher
     */
    public void setEventPublisher(EventPublisher eventPublisher)
    {
        this.eventPublisher = eventPublisher;
    }

    /**
     * Sets the rendition service.
     *
     * @deprecated The RenditionService is being replace by the simpler async RenditionService2.
     */
    @Deprecated
    public void setrenditionService(RenditionService renditionService)
    {
        this.renditionService = renditionService;
    }

    /**
     * Sets the file folder service.
     */
    public void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }

    public FileFolderService getFileFolderService()
    {
        return fileFolderService;
    }

    /**
     * Sets the tenant admin service.
     */
    public void setTenantAdminService(TenantAdminService tenantAdminService)
    {
        this.tenantAdminService = tenantAdminService;
    }

    public void setSingletonCache(SimpleCache<String, Object> singletonCache)
    {
        this.singletonCache = singletonCache;
    }

    /**
     * Sets the transaction service.
     */
    public void setTransactionService(TransactionService transactionService)
    {
        this.transactionService = transactionService;
    }

    public TransactionService getTransactionService()
    {
        return transactionService;
    }

    /**
     * Sets the authentication service.
     */
    public void setAuthenticationService(AuthenticationService authenticationService)
    {
        this.authenticationService = authenticationService;
    }

    public AuthenticationService getAuthenticationService()
    {
        return authenticationService;
    }

    /**
     * Sets the permission service.
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }

    /**
     * Sets the permission model DAO.
     */
    public void setPermissionModelDao(ModelDAO permissionModelDao)
    {
        this.permissionModelDao = permissionModelDao;
    }

    public void setOpenCMISDictionaryService(CMISDictionaryService cmisDictionaryService)
    {
        this.cmisDictionaryService = cmisDictionaryService;
    }

    public void setOpenCMISDictionaryService11(CMISDictionaryService cmisDictionaryService)
    {
        this.cmisDictionaryService11 = cmisDictionaryService;
    }

    public CMISDictionaryService getOpenCMISDictionaryService()
    {
        CmisVersion cmisVersion = getRequestCmisVersion();
        if(cmisVersion.equals(CmisVersion.CMIS_1_0))
        {
            return cmisDictionaryService;
        }
        else
        {
            return cmisDictionaryService11;
        }
    }

    /**
     * Sets the OpenCMIS query service.
     */
    public void setOpenCMISQueryService(CMISQueryService cmisQueryService)
    {
        this.cmisQueryService = cmisQueryService;
    }

    public void setOpenCMISQueryService11(CMISQueryService cmisQueryService)
    {
        this.cmisQueryService11 = cmisQueryService;
    }

    public CMISQueryService getOpenCMISQueryService()
    {
        CmisVersion cmisVersion = getRequestCmisVersion();
        if(cmisVersion.equals(CmisVersion.CMIS_1_0))
        {
            return cmisQueryService;
        }
        else
        {
            return cmisQueryService11;
        }
    }

    /**
     * Sets the MIME type service.
     */
    public void setMimetypeService(MimetypeService mimetypeService)
    {
        this.mimetypeService = mimetypeService;
    }

    public MimetypeService getMimetypeService()
    {
        return mimetypeService;
    }

    /**
     * Sets the audit service.
     */
    public void setAuditService(AuditService auditService)
    {
        this.auditService = auditService;
    }

    /**
     * Sets the namespace service.
     */
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }

    /**
     * Sets the search service.
     */
    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }

    public SearchService getSearchService()
    {
        return searchService;
    }

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

    public DictionaryService getDictionaryService()
    {
        return dictionaryService;
    }

    /**
     * Not implemented
     * @throws UnsupportedOperationException always
     */
    public void setProxyUser(String proxyUser)
    {
//        this.proxyUser = proxyUser;
        throw new UnsupportedOperationException("proxyUser setting not implemented.  Please raise a JIRA request.");
    }

    public String getProxyUser()
    {
        return proxyUser;
    }

    /**
     * Sets bulk update properties.
     */
    public void setBulkMaxItems(int size)
    {
        bulkMaxItems = size;
    }

    public int getBulkMaxItems()
    {
        return bulkMaxItems;
    }

    public void setBulkBatchSize(int size)
    {
        bulkBatchSize = size;
    }

    public int getBulkBatchSize()
    {
        return bulkBatchSize;
    }

    public void setBulkWorkerThreads(int threads)
    {
        bulkWorkerThreads = threads;
    }

    public int getBulkWorkerThreads()
    {
        return bulkWorkerThreads;
    }

    // --------------------------------------------------------------
    // Lifecycle methods
    // --------------------------------------------------------------

    private File tmp;

    public void setup()
    {
    	File tempDir = TempFileProvider.getTempDir();
    	this.tmp = new File(tempDir, "CMISAppend");
    	if(!this.tmp.exists() && !this.tmp.mkdir())
    	{
    		throw new AlfrescoRuntimeException("Failed to create CMIS temporary directory");
    	}
    }

    public void init()
    {
        // register as tenant deployer
        tenantAdminService.register(this);

        // set up and cache rendition mapping
        getRenditionMapping();

        // cache root node ref
        getRootNodeRef();

        // cache permission definitions and permission mappings
        repositoryPermissions = getRepositoryPermissions();
        permissionMappings = getPermissionMappings();
    }

    public void destroy()
    {
        singletonCache.remove(KEY_CMIS_ROOT_NODEREF);
        singletonCache.remove(KEY_CMIS_RENDITION_MAPPING_NODEREF);
    }

    public void onEnableTenant()
    {
        init();
    }

    public void onDisableTenant()
    {
        destroy();
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        lifecycle.setApplicationContext(applicationContext);
    }

    public void onApplicationEvent(ApplicationContextEvent event)
    {
        lifecycle.onApplicationEvent(event);
    }

    /**
     * Hooks into Spring Application Lifecycle.
     */
    private class ProcessorLifecycle extends AbstractLifecycleBean
    {
        @Override
        protected void onBootstrap(ApplicationEvent event)
        {
            init();
        }

        @Override
        protected void onShutdown(ApplicationEvent event)
        {
        }
    }

    // --------------------------------------------------------------
    // Alfresco methods
    // --------------------------------------------------------------

    /*
     * For the given cmis property name get the corresponding Alfresco property name.
     *
     * Certain CMIS properties (e.g. cmis:creationDate and cmis:lastModifiedBy) don't
     * have direct mappings to Alfresco properties through the CMIS dictionary, because
     * these mappings should not be exposed outside the repository through CMIS. For these,
     * however, this method provides the mapping so that the sort works.
     *
     */
    public Pair<QName, Boolean> getSortProperty(String cmisPropertyName, String direction)
    {
    	QName sortPropName = null;
    	Pair<QName, Boolean> sortProp = null;

        PropertyDefinitionWrapper propDef = getOpenCMISDictionaryService().findPropertyByQueryName(cmisPropertyName);
        if (propDef != null)
        {
	        if (propDef.getPropertyId().equals(PropertyIds.BASE_TYPE_ID))
	        {
	            // special-case (see also ALF-13968) - for getChildren, using "cmis:baseTypeId" allows sorting of folders first and vice-versa (cmis:folder <-> cmis:document)
	        	sortPropName = GetChildrenCannedQuery.SORT_QNAME_NODE_IS_FOLDER;
	        }
	        else
	        {
	        	sortPropName = propDef.getPropertyAccessor().getMappedProperty();
	        }

	        if (sortPropName == null)
	        {
	        	// ok to map these properties because we are always getting current versions of nodes
	        	sortPropName = SORT_PROPERTY_MAPPINGS.get(cmisPropertyName);
	        }
	    }

    	if (sortPropName != null)
    	{
    		boolean sortAsc = (direction == null ? true : direction.equalsIgnoreCase("asc"));
    		sortProp = new Pair<QName, Boolean>(sortPropName, sortAsc);
    	}
    	else
    	{
    		if (logger.isDebugEnabled())
    		{
    			logger.debug("Ignore sort property '" + cmisPropertyName + " - mapping not found");
    		}
    	}

        return sortProp;
    }

    /**
     * Asynchronously generates thumbnails for the given node.
     *
     * @param nodeRef NodeRef
     */
    public void createThumbnails(NodeRef nodeRef, Set<String> thumbnailNames)
    {
    	if(thumbnailNames == null || thumbnailNames.size() == 0)
    	{
    		return;
    	}

        ThumbnailRegistry registry = thumbnailService.getThumbnailRegistry();

        // If there's nothing currently registered to generate thumbnails for the
        //  specified mimetype, then log a message and bail out
        Serializable value = this.nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
        ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, value);
        if (contentData == null)
        {
        	logger.info("Unable to create thumbnails as there is no content");
        	return;
        }

    	long size = contentData.getSize();
    	String mimeType = contentData.getMimetype();

    	for(String thumbnailName : thumbnailNames)
    	{
	        // Use the thumbnail registy to get the details of the thumbail
	        ThumbnailDefinition details = registry.getThumbnailDefinition(thumbnailName);
	        if(details == null)
	        {
	            // Throw exception
	            logger.warn("The thumbnail name '" + thumbnailName + "' is not registered");
	            continue;
	        }
	        else
	        {
		        if(registry.isThumbnailDefinitionAvailable(contentData.getContentUrl(), mimeType, size, nodeRef, details))
		        {
		            org.alfresco.service.cmr.action.Action action = ThumbnailHelper.createCreateThumbnailAction(details, serviceRegistry);

		            // Queue async creation of thumbnail
		            actionService.executeAction(action, nodeRef, true, true);
		        }
		        else
		        {
		        	logger.info("Unable to create thumbnail '" + details.getName() + "' for " +
		        			mimeType + " as no transformer is currently available");
		        }
	        }
    	}
    }

    /**
     * Extracts metadata for the node.
     *
     * @param nodeRef NodeRef
     */
    public void extractMetadata(NodeRef nodeRef)
    {
    	org.alfresco.service.cmr.action.Action action = actionService.createAction(ContentMetadataExtracter.EXECUTOR_NAME);
    	actionService.executeAction(action, nodeRef, true, false);
    }

    public SiteInfo getSite(NodeRef nodeRef)
    {
    	return siteService.getSite(nodeRef);
    }

    /**
     *  Should the node be filtered?
     */
	public boolean filter(NodeRef nodeRef)
    {
    	return objectFilter.filter(nodeRef);
    }

    public boolean disableBehaviour(QName className)
    {
        boolean wasEnabled = behaviourFilter.isEnabled(className);
        if(wasEnabled)
        {
        	behaviourFilter.disableBehaviour(className);
        }
        return wasEnabled;
    }

    public boolean enableBehaviour(QName className)
    {
        boolean isEnabled = behaviourFilter.isEnabled(className);
        if(!isEnabled)
        {
            behaviourFilter.enableBehaviour(className);
        }
        return isEnabled;
    }


    public boolean disableBehaviour(QName className, NodeRef nodeRef)
    {
        boolean wasEnabled = behaviourFilter.isEnabled(nodeRef, className);
        if(wasEnabled)
        {
            behaviourFilter.disableBehaviour(nodeRef, className);
        }
        return wasEnabled;
    }

    public boolean enableBehaviour(QName className, NodeRef nodeRef)
    {
        boolean isEnabled = behaviourFilter.isEnabled(nodeRef, className);
        if(!isEnabled)
        {
            behaviourFilter.enableBehaviour(nodeRef, className);
        }
        return isEnabled;
    }

    public StoreRef getRootStoreRef()
    {
        return getRootNodeRef().getStoreRef();
    }

    public void deleteNode(NodeRef nodeRef, boolean postActivity)
    {
        ActivityInfo activityInfo = null;

        // post activity after removal of the node
        postActivity &= !hiddenAspect.hasHiddenAspect(nodeRef);
    	if(postActivity)
    	{
        	// get this information before the node is deleted
	    	activityInfo = activityPoster.getActivityInfo(nodeRef);
    	}

        getNodeService().deleteNode(nodeRef);

        // post activity after removal of the node
        if(postActivity && activityInfo != null)
        {
        	activityPoster.postFileFolderDeleted(activityInfo);
        }
    }

    public RetryingTransactionHelper getRetryingTransactionHelper()
    {
    	return transactionService.getRetryingTransactionHelper();
    }

    /**
     * Returns the root folder node ref.
     */
    public NodeRef getRootNodeRef()
    {
        NodeRef rootNodeRef = (NodeRef)singletonCache.get(KEY_CMIS_ROOT_NODEREF);
        if (rootNodeRef == null)
        {
            rootNodeRef = AuthenticationUtil.runAs(new RunAsWork<NodeRef>()
            {
                public NodeRef doWork() throws Exception
                {
                    return transactionService.getRetryingTransactionHelper().doInTransaction(
                            new RetryingTransactionCallback<NodeRef>()
                            {
                                public NodeRef execute() throws Exception
                                {
                                    NodeRef root = nodeService.getRootNode(storeRef);
                                    List<NodeRef> rootNodes = searchService.selectNodes(root, rootPath, null,
                                            namespaceService, false);
                                    if (rootNodes.size() != 1)
                                    {
                                        throw new CmisRuntimeException("Unable to locate CMIS root path " + rootPath);
                                    }
                                    return rootNodes.get(0);
                                };
                            }, true);
                }
            }, AuthenticationUtil.getSystemUserName());

            if (rootNodeRef == null)
            {
                throw new CmisObjectNotFoundException("Root folder path '" + rootPath + "' not found!");
            }

            singletonCache.put(KEY_CMIS_ROOT_NODEREF, rootNodeRef);
        }

        return rootNodeRef;
    }

    public String getName(NodeRef nodeRef)
    {
        try
        {
            Object name = nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
            return (name instanceof String ? (String) name : null);
        }
        catch(InvalidNodeRefException inre)
        {
            return null;
        }
    }

    /**
     * Cuts of the version information from an object id.
     */
    public String getCurrentVersionId(String objectId)
    {
        if (objectId == null)
        {
            return null;
        }

        int sepIndex = objectId.lastIndexOf(ID_SEPERATOR);
        if (sepIndex > -1)
        {
            return objectId.substring(0, sepIndex);
        }

        return objectId;
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(String objectId)
    {
        return new CMISNodeInfoImpl(this, objectId);
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef)
    {
        return createNodeInfo(nodeRef, null, null, null, true);
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef, QName nodeType, Map<QName,Serializable> nodeProps, VersionHistory versionHistory, boolean checkExists)
    {
        return new CMISNodeInfoImpl(this, nodeRef, nodeType, nodeProps, versionHistory, checkExists);
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(AssociationRef assocRef)
    {
        return new CMISNodeInfoImpl(this, assocRef);
    }

    /*
     * Strip store ref from the id, if there is one.
     */
    private String getGuid(String id)
    {
    	int idx = id.lastIndexOf("/");
    	if(idx != -1)
    	{
    		return id.substring(idx + 1);
    	}
    	else
    	{
    		return id;
    	}
    }

    /*
     * Construct an object id based on the incoming assocRef and versionLabel. The object id will always
     * be the assocRef guid.
     */
    public String constructObjectId(AssociationRef assocRef, String versionLabel)
    {
        return constructObjectId(assocRef, versionLabel, isPublicApi());
    }

    public String constructObjectId(AssociationRef assocRef, String versionLabel, boolean dropStoreRef)
    {
    	StringBuilder sb = new StringBuilder(CMISConnector.ASSOC_ID_PREFIX);
    	if(dropStoreRef)
    	{
    		// always return the guid
    		sb.append(assocRef.getId());
    	}
    	else
    	{
    		sb.append(assocRef.toString());
    	}

    	if(versionLabel != null)
    	{
    		sb.append(CMISConnector.ID_SEPERATOR);
    		sb.append(versionLabel);
    	}
    	return sb.toString();
    }

    /*
     * Construct an object id based on the incoming incomingObjectId. The object id will always
     * be the node guid.
     */
    public String constructObjectId(String incomingObjectId)
    {
    	return constructObjectId(incomingObjectId, null);
    }

    /*
     * Construct an object id based on the incoming incomingNodeId and versionLabel. The object id will always
     * be the node guid.
     */
    public String constructObjectId(String incomingNodeId, String versionLabel)
    {
        return constructObjectId(incomingNodeId, versionLabel, isPublicApi());
    }

    public String constructObjectId(String incomingNodeId, String versionLabel, boolean dropStoreRef)
    {
    	StringBuilder sb = new StringBuilder();
    	if(dropStoreRef)
    	{
    		// always return the guid
    		sb.append(getGuid(incomingNodeId));
    	}
    	else
    	{
    	    sb.append(incomingNodeId);
    	}
    	if(versionLabel != null)
    	{
    		sb.append(CMISConnector.ID_SEPERATOR);
    		sb.append(versionLabel);
    	}
    	return sb.toString();
    }

    public void createVersion(NodeRef nodeRef, VersionType versionType, String reason)
    {
    	if(versionService.getVersionHistory(nodeRef) == null)
    	{
    		// no version history. Make sure we have an initial major version 1.0.
            Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(2);
            versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR);
            versionProperties.put(VersionModel.PROP_DESCRIPTION, "Initial version");
            versionService.createVersion(nodeRef, versionProperties);
    	}

        Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(2);
        versionProperties.put(VersionModel.PROP_VERSION_TYPE, versionType);
        versionProperties.put(VersionModel.PROP_DESCRIPTION, reason);
        versionService.createVersion(nodeRef, versionProperties);
    }

    public CmisVersion getRequestCmisVersion()
    {
        CallContext callContext = AlfrescoCmisServiceCall.get();
        CmisVersion cmisVersion = (callContext != null ? callContext.getCmisVersion() : CmisVersion.CMIS_1_0);
        return cmisVersion;
    }

    private boolean isPublicApi()
    {
    	boolean isPublicApi = false;
    	CallContext callContext = AlfrescoCmisServiceCall.get();
    	if(callContext != null)
    	{
	    	String value = (String)callContext.get("isPublicApi");
	    	isPublicApi = (value == null ? false : Boolean.parseBoolean(value));
    	}
    	return isPublicApi;
    }

    /*
     * Construct an object id based on the incoming incomingNodeRef and versionLabel. The object id will always
     * be the incomingNodeRef guid.
     */
    public String constructObjectId(NodeRef incomingNodeRef, String versionLabel)
    {
        return constructObjectId(incomingNodeRef, versionLabel, isPublicApi());
    }

    public String constructObjectId(NodeRef incomingNodeRef, String versionLabel, boolean dropStoreRef)
    {
    	StringBuilder sb = new StringBuilder();
    	sb.append(dropStoreRef ? incomingNodeRef.getId() : incomingNodeRef.toString());
    	if(versionLabel != null)
    	{
    		sb.append(CMISConnector.ID_SEPERATOR);
    		sb.append(versionLabel);
    	}
    	return sb.toString();
    }

    /**
     * Compiles a CMIS object if for a live node.
     */
    public String createObjectId(NodeRef nodeRef)
    {
        return createObjectId(nodeRef, isPublicApi());
    }

    public String createObjectId(NodeRef nodeRef, boolean dropStoreRef)
    {
    	QName typeQName = nodeService.getType(nodeRef);
        TypeDefinitionWrapper type = getOpenCMISDictionaryService().findNodeType(typeQName);

    	if(type instanceof ItemTypeDefinitionWrapper)
    	{
    		return constructObjectId(nodeRef, null);
    	}

        if(type instanceof FolderTypeDefintionWrapper)
        {
            return constructObjectId(nodeRef, null, dropStoreRef);
        }

        Serializable versionLabel = getNodeService()
                .getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL);
        if (versionLabel == null)
        {
        	versionLabel = CMISConnector.UNVERSIONED_VERSION_LABEL;
        }

        return constructObjectId(nodeRef, (String)versionLabel, dropStoreRef);
    }

    private boolean isFolder(NodeRef nodeRef)
    {
        return getType(nodeRef) instanceof FolderTypeDefintionWrapper;
    }

    /**
     * Returns the type definition of a node or <code>null</code> if no type
     * definition could be found.
     */
    public TypeDefinitionWrapper getType(NodeRef nodeRef)
    {
        try
        {
            QName typeQName = nodeService.getType(nodeRef);
            return getOpenCMISDictionaryService().findNodeType(typeQName);
        }
        catch(InvalidNodeRefException inre)
        {
            return null;
        }
    }

    /**
     * Returns the type definition of an association or <code>null</code> if no
     * type definition could be found.
     */
    public TypeDefinitionWrapper getType(AssociationRef assocRef)
    {
        QName typeQName = assocRef.getTypeQName();
        return getOpenCMISDictionaryService().findAssocType(typeQName);
    }

    /**
     * Returns the type definition of a node or <code>null</code> if no type
     * definition could be found.
     */
    public TypeDefinitionWrapper getType(String cmisTypeId)
    {
        return getOpenCMISDictionaryService().findType(cmisTypeId);
    }

    /**
     * Returns the definition after it has checked if the type can be used for
     * object creation.
     */
    public TypeDefinitionWrapper getTypeForCreate(String cmisTypeId, BaseTypeId baseTypeId)
    {
        TypeDefinitionWrapper type = getType(cmisTypeId);
        if ((type == null) || (type.getBaseTypeId() != baseTypeId))
        {
            switch (baseTypeId)
            {
            case CMIS_DOCUMENT:
                throw new CmisConstraintException("Type is not a document type!");
            case CMIS_FOLDER:
                throw new CmisConstraintException("Type is not a folder type!");
            case CMIS_RELATIONSHIP:
                throw new CmisConstraintException("Type is not a relationship type!");
            case CMIS_POLICY:
                throw new CmisConstraintException("Type is not a policy type!");
            case CMIS_ITEM:
                throw new CmisConstraintException("Type is not an item type!");
            }
        }

        if (!type.getTypeDefinition(false).isCreatable())
        {
            throw new CmisConstraintException("Type is not creatable!");
        }

        return type;
    }

    /**
     * Applies a versioning state to a document.
     */
    public void applyVersioningState(NodeRef nodeRef, VersioningState versioningState)
    {
        if (versioningState == VersioningState.CHECKEDOUT)
        {
            if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE))
            {
                nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null);
            }

            // MNT-14687 : Creating a document as checkedout and then cancelling the checkout should delete the document
            nodeService.addAspect(nodeRef, ContentModel.ASPECT_CMIS_CREATED_CHECKEDOUT, null);

            getCheckOutCheckInService().checkout(nodeRef);
        }
        else if ((versioningState == VersioningState.MAJOR) || (versioningState == VersioningState.MINOR))
        {
            if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE))
            {
                nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null);
            }

            Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(5);
            versionProperties.put(
                    VersionModel.PROP_VERSION_TYPE,
                    versioningState == VersioningState.MAJOR ? VersionType.MAJOR : VersionType.MINOR);
            versionProperties.put(VersionModel.PROP_DESCRIPTION, "Initial Version");

            versionService.createVersion(nodeRef, versionProperties);
        }
    }

    /**
     * Checks if a child of a given type can be added to a given folder.
     */
    @SuppressWarnings("unchecked")
    public void checkChildObjectType(CMISNodeInfo folderInfo, String childType)
    {
        TypeDefinitionWrapper targetType = folderInfo.getType();
        PropertyDefinitionWrapper allowableChildObjectTypeProperty = targetType
                .getPropertyById(PropertyIds.ALLOWED_CHILD_OBJECT_TYPE_IDS);
        List<String> childTypes = (List<String>) allowableChildObjectTypeProperty.getPropertyAccessor().getValue(
                folderInfo);

        if ((childTypes == null) || childTypes.isEmpty())
        {
            return;
        }

        if (!childTypes.contains(childType))
        {
            throw new CmisConstraintException("Objects of type '" + childType + "' cannot be added to this folder!");
        }
    }

    /**
     * Creates the CMIS object for a node.
     */
    public ObjectData createCMISObject(CMISNodeInfo info, String filter, boolean includeAllowableActions,
            IncludeRelationships includeRelationships, String renditionFilter, boolean includePolicyIds,
            boolean includeAcl)
    {
        if (info.getType() == null)
        {
            throw new CmisObjectNotFoundException("No corresponding type found! Not a CMIS object?");
        }

        Properties nodeProps = (info.isRelationship() ? getAssocProperties(info, filter) : getNodeProperties(info, filter));

        return createCMISObjectImpl(info, nodeProps, filter, includeAllowableActions, includeRelationships,
                renditionFilter, includePolicyIds, includeAcl);
    }

    @SuppressWarnings("unchecked")
    private ObjectData createCMISObjectImpl(final CMISNodeInfo info, Properties nodeProps, String filter,
            boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            boolean includePolicyIds, boolean includeAcl)
    {
        final ObjectDataImpl result = new ObjectDataImpl();

        // set allowable actions
        if (includeAllowableActions)
        {
            result.setAllowableActions(getAllowableActions(info));
        }

        // set policy ids
        if (includePolicyIds)
        {
            result.setPolicyIds(new PolicyIdListImpl());
        }

        if (info.isRelationship())
        {
            // set properties
            result.setProperties(getAssocProperties(info, filter));

            // set ACL
            if (includeAcl)
            {
                // association have no ACL - return an empty list of ACEs
                result.setAcl(new AccessControlListImpl((List<Ace>) Collections.EMPTY_LIST));
                result.setIsExactAcl(Boolean.FALSE);
            }
        }
        else
        {
            // set properties
            result.setProperties(nodeProps);

            // set relationships
            if (includeRelationships != IncludeRelationships.NONE)
            {
                result.setRelationships(getRelationships(info.getNodeRef(), includeRelationships));
            }

            // set renditions
            if (!RENDITION_NONE.equals(renditionFilter))
            {
                List<RenditionData> renditions = getRenditions(info.getNodeRef(), renditionFilter, null, null);
                if ((renditions != null) && (!renditions.isEmpty()))
                {
                    result.setRenditions(renditions);
                }
                else
                {
                	result.setRenditions(Collections.EMPTY_LIST);
                }
            }

            // set ACL
            if (includeAcl)
            {
            	AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
            	{
					@Override
					public Void doWork() throws Exception
					{
					    Acl acl = getACL(info.getCurrentNodeNodeRef(), false);
		                if (acl != null)
		                {
					        result.setAcl(acl);
					        result.setIsExactAcl(acl.isExact());
		                }
						return null;
					}
            	});
            }

            // add aspects
            List<CmisExtensionElement> extensions = getAspectExtensions(info, filter, result.getProperties()
                    .getProperties().keySet());

            if (!extensions.isEmpty())
            {
                result.getProperties().setExtensions(
                        Collections.singletonList((CmisExtensionElement) new CmisExtensionElementImpl(
                                ALFRESCO_EXTENSION_NAMESPACE, ASPECTS, null, extensions)));
            }
        }
        return result;
    }

    public String getPath(NodeRef nodeRef)
    {
        try
        {
            Path path = nodeService.getPath(nodeRef);

            // skip to CMIS root path
            NodeRef rootNode = getRootNodeRef();
            int i = 0;
            while (i < path.size())
            {
                Path.Element element = path.get(i);
                if (element instanceof ChildAssocElement)
                {
                    ChildAssociationRef assocRef = ((ChildAssocElement) element).getRef();
                    NodeRef node = assocRef.getChildRef();
                    if (node.equals(rootNode))
                    {
                        break;
                    }
                }
                i++;
            }

            StringBuilder displayPath = new StringBuilder(64);

            if (path.size() - i == 1)
            {
                // render root path
                displayPath.append("/");
            }
            else
            {
                // render CMIS scoped path
                i++;
                while (i < path.size())
                {
                    Path.Element element = path.get(i);
                    if (element instanceof ChildAssocElement)
                    {
                        ChildAssociationRef assocRef = ((ChildAssocElement) element).getRef();
                        NodeRef node = assocRef.getChildRef();
                        displayPath.append("/");
                        displayPath.append(nodeService.getProperty(node, ContentModel.PROP_NAME));
                    }
                    i++;
                }
            }

            return displayPath.toString();
        }
        catch(InvalidNodeRefException inre)
        {
            return null;
        }
    }

    /**
     * Gets the content from the repository.
     */
    public ContentStream getContentStream(CMISNodeInfo info, String streamId, BigInteger offset, BigInteger length)
    {
        // get the type and check if the object can have content
        TypeDefinitionWrapper type = info.getType();
        checkDocumentTypeForContent(type);

        // looks like a document, now get the content
        ContentStreamImpl result = new ContentStreamImpl();
        result.setFileName(info.getName());

        // if streamId is set, fetch other content
        NodeRef streamNodeRef = info.getNodeRef();
        if ((streamId != null) && (streamId.length() > 0))
        {
            CMISNodeInfo streamInfo = createNodeInfo(streamId);
            if (!streamInfo.isVariant(CMISObjectVariant.CURRENT_VERSION))
            {
                throw new CmisInvalidArgumentException("Stream id is invalid: " + streamId + ", expected variant " + CMISObjectVariant.CURRENT_VERSION + ", got variant " + streamInfo.getObjectVariant());
            }

            streamNodeRef = streamInfo.getNodeRef();
            type = streamInfo.getType();
            checkDocumentTypeForContent(type);
        }

        // get the stream now
        try
        {
            ContentReader contentReader = contentService.getReader(streamNodeRef, ContentModel.PROP_CONTENT);
            if (contentReader == null)
            {
                throw new CmisConstraintException("Document has no content!");
            }

            result.setMimeType(contentReader.getMimetype());
            long contentSize = contentReader.getSize();

            if ((offset == null) && (length == null))
            {
                result.setStream(contentReader.getContentInputStream());
                result.setLength(BigInteger.valueOf(contentSize));
                publishReadEvent(streamNodeRef, info.getName(), result.getMimeType(), contentSize, contentReader.getEncoding(), null);
            }
            else
            {
                long off = (offset == null ? 0 : offset.longValue());
                long len = (length == null ? contentSize : length.longValue());
                if (off + len > contentSize)
                {
                    len = contentReader.getSize() - off;
                }

                result.setStream(new RangeInputStream(contentReader.getContentInputStream(), off, len));
                result.setLength(BigInteger.valueOf(len));
                publishReadEvent(streamNodeRef, info.getName(), result.getMimeType(), contentSize, contentReader.getEncoding(), off+" - "+len);
            }
        }
        catch (Exception e)
        {
            if (e instanceof CmisBaseException)
            {
                throw (CmisBaseException) e;
            }
            else
            {
                StringBuilder msg = new StringBuilder("Failed to retrieve content: " + e.getMessage());
                Throwable cause = e.getCause();
                if(cause != null)
                {
                    // add the cause to the CMIS exception
                    msg.append(", ");
                    msg.append(cause.getMessage());
                }
                throw new CmisRuntimeException(msg.toString(), e);
            }
        }

        return result;
    }

    /**
     * Notifies listeners that a read has taken place.
     *
     * @param nodeRef NodeRef
     * @param name String
     * @param mimeType String
     * @param contentSize long
     * @param encoding String
     * @param range String
     */
    protected void publishReadEvent(final NodeRef nodeRef, final String name, final String mimeType, final long contentSize, final String encoding, final String range)
    {
        final QName nodeType = nodeRef==null?null:nodeService.getType(nodeRef);

        eventPublisher.publishEvent(new EventPreparator(){
            @Override
            public Event prepareEvent(String user, String networkId, String transactionId)
            {
                if (StringUtils.hasText(range))
                {
                    return new ContentReadRangeEvent(user, networkId, transactionId,
                                nodeRef.getId(), null, nodeType.toString(), Client.asType(ClientType.cmis), name, mimeType, contentSize, encoding, range);
                }
                else
                {
                    return new ContentEventImpl(ContentEvent.DOWNLOAD, user, networkId, transactionId,
                                nodeRef.getId(), null, nodeType.toString(), Client.asType(ClientType.cmis), name, mimeType, contentSize, encoding);
                }
            }
        });


    }

    public void appendContent(CMISNodeInfo nodeInfo, ContentStream contentStream, boolean isLastChunk) throws IOException
    {
    	NodeRef nodeRef = nodeInfo.getNodeRef();

    	this.disableBehaviour(ContentModel.ASPECT_VERSIONABLE, nodeRef);

    	if(!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_CMIS_UPDATE_CONTEXT))
    	{
    		Map<QName, Serializable> props = new HashMap<QName, Serializable>();
    		props.put(ContentModel.PROP_GOT_FIRST_CHUNK, true);
    		nodeService.addAspect(nodeRef, ContentModel.ASPECT_CMIS_UPDATE_CONTEXT, props);
    	}

		ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
		InputStream existingContentInput = (reader != null ? reader.getContentInputStream() : null);
		InputStream input = contentStream.getStream();

    	ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
    	OutputStream out = new BufferedOutputStream(writer.getContentOutputStream());

    	InputStream in = null;
    	if(existingContentInput != null)
    	{
    		in = new SequenceInputStream(existingContentInput, input);
    	}
    	else
    	{
    		in = input;
    	}

		try
		{
	    	IOUtils.copy(in, out);

	    	if(isLastChunk)
	    	{
    			nodeService.removeAspect(nodeRef, ContentModel.ASPECT_CMIS_UPDATE_CONTEXT);
	    		getActivityPoster().postFileFolderUpdated(nodeInfo.isFolder(), nodeRef);

	    		createVersion(nodeRef, VersionType.MINOR, "Appended content stream");
	    	}
		}
		finally
		{
			if(in != null)
			{
				in.close();
			}
			if(out != null)
			{
    	    	out.close();
			}

			this.enableBehaviour(ContentModel.ASPECT_VERSIONABLE, nodeRef);
		}
    }

    private void checkDocumentTypeForContent(TypeDefinitionWrapper type)
    {
        if (type == null)
        {
            throw new CmisObjectNotFoundException("No corresponding type found! Not a CMIS object?");
        }
        if (!(type instanceof DocumentTypeDefinitionWrapper))
        {
            throw new CmisStreamNotSupportedException("Object is not a document!");
        }
        if (((DocumentTypeDefinition) type.getTypeDefinition(false)).getContentStreamAllowed() == ContentStreamAllowed.NOTALLOWED)
        {
            throw new CmisConstraintException("Document cannot have content!");
        }
    }

    private void addAspectProperties(CMISNodeInfo info, String filter, PropertiesImpl result)
    {
        if (getRequestCmisVersion().equals(CmisVersion.CMIS_1_1))
        {
            Set<String> propertyIds = new HashSet<>();
            Set<String> filterSet = splitFilter(filter);

            Set<QName> aspects = info.getNodeAspects();
            for (QName aspect : aspects)
            {
                TypeDefinitionWrapper aspectType = getOpenCMISDictionaryService().findNodeType(aspect);
                if (aspectType == null)
                {
                    continue;
                }

                for (PropertyDefinitionWrapper propDef : aspectType.getProperties())
                {
                    if (propertyIds.contains(propDef.getPropertyId()))
                    {
                        // skip properties that have already been added
                        continue;
                    }

                    if ((filterSet != null) && (!filterSet.contains(propDef.getPropertyDefinition().getQueryName())))
                    {
                        // skip properties that are not in the filter
                        continue;
                    }

                    Serializable value = propDef.getPropertyAccessor().getValue(info);
                    result.addProperty(getProperty(propDef.getPropertyDefinition().getPropertyType(), propDef, value));

                    // mark property as 'added'
                    propertyIds.add(propDef.getPropertyId());
                }
            }
        }
    }

    public Properties getNodeProperties(CMISNodeInfo info, String filter)
    {
        PropertiesImpl result = new PropertiesImpl();

        Set<String> filterSet = splitFilter(filter);

        for (PropertyDefinitionWrapper propDef : info.getType().getProperties())
        {
            if (!propDef.getPropertyId().equals(PropertyIds.OBJECT_ID))
            {
                // don't filter the object id
                if ((filterSet != null) && (!filterSet.contains(propDef.getPropertyDefinition().getQueryName())))
                {
                    // skip properties that are not in the filter
                    continue;
                }
            }

            Serializable value = propDef.getPropertyAccessor().getValue(info);
            result.addProperty(getProperty(propDef.getPropertyDefinition().getPropertyType(), propDef, value));
        }

        addAspectProperties(info, filter, result);

        return result;
    }

    public Properties getAssocProperties(CMISNodeInfo info, String filter)
    {
        PropertiesImpl result = new PropertiesImpl();

        Set<String> filterSet = splitFilter(filter);

        for (PropertyDefinitionWrapper propDefWrap : info.getType().getProperties())
        {
            PropertyDefinition<?> propDef = propDefWrap.getPropertyDefinition();
            if ((filterSet != null) && (!filterSet.contains(propDef.getQueryName())))
            {
                // skip properties that are not in the filter
                continue;
            }

            CMISPropertyAccessor cmisPropertyAccessor = propDefWrap.getPropertyAccessor();
            Serializable value = cmisPropertyAccessor.getValue(info);
            PropertyType propType = propDef.getPropertyType();
            PropertyData<?> propertyData = getProperty(propType, propDefWrap, value);
            result.addProperty(propertyData);
        }

        return result;
    }

    /**
     * Builds aspect extension.
     */
    public List<CmisExtensionElement> getAspectExtensions(CMISNodeInfo info, String filter, Set<String> alreadySetProperties)
    {
        List<CmisExtensionElement> extensions = new ArrayList<CmisExtensionElement>();
        Set<String> propertyIds = new HashSet<String>(alreadySetProperties);
        List<CmisExtensionElement> propertyExtensionList = new ArrayList<CmisExtensionElement>();
        Set<String> filterSet = splitFilter(filter);

        Set<QName> aspects = info.getNodeAspects();
        for (QName aspect : aspects)
        {
            TypeDefinitionWrapper aspectType = getOpenCMISDictionaryService().findNodeType(aspect);
            if (aspectType == null)
            {
                continue;
            }

            AspectDefinition aspectDefinition = dictionaryService.getAspect(aspect);
            Map<QName, org.alfresco.service.cmr.dictionary.PropertyDefinition> aspectProperties = aspectDefinition.getProperties();

            extensions.add(new CmisExtensionElementImpl(ALFRESCO_EXTENSION_NAMESPACE, APPLIED_ASPECTS, null, aspectType
                    .getTypeId()));

            for (PropertyDefinitionWrapper propDef : aspectType.getProperties())
            {
                boolean addPropertyToExtensionList = getRequestCmisVersion().equals(CmisVersion.CMIS_1_1) && aspectProperties.keySet().contains(propDef.getAlfrescoName());
                // MNT-11876 : add property to extension even if it has been returned (CMIS 1.1)
                if (propertyIds.contains(propDef.getPropertyId()) && !addPropertyToExtensionList)
                {
                    // skip properties that have already been added
                    continue;
                }

                if ((filterSet != null) && (!filterSet.contains(propDef.getPropertyDefinition().getQueryName())))
                {
                    // skip properties that are not in the filter
                    continue;
                }

                Serializable value = propDef.getPropertyAccessor().getValue(info);
                propertyExtensionList.add(createAspectPropertyExtension(propDef.getPropertyDefinition(), value));

                // mark property as 'added'
                propertyIds.add(propDef.getPropertyId());
            }
        }

        if (!propertyExtensionList.isEmpty())
        {
            CmisExtensionElementImpl propertiesExtension = new CmisExtensionElementImpl(
                    ALFRESCO_EXTENSION_NAMESPACE, "properties", null, propertyExtensionList);
            extensions.addAll(Collections.singletonList(propertiesExtension));
        }

        return extensions;
    }

    /**
     * Creates a property extension element.
     */
    @SuppressWarnings("rawtypes")
    private CmisExtensionElement createAspectPropertyExtension(PropertyDefinition<?> propertyDefinition, Object value)
    {
        String name;
        switch (propertyDefinition.getPropertyType())
        {
        case BOOLEAN:
            name = "propertyBoolean";
            break;
        case DATETIME:
            name = "propertyDateTime";
            break;
        case DECIMAL:
            name = "propertyDecimal";
            break;
        case INTEGER:
            name = "propertyInteger";
            break;
        case ID:
            name = "propertyId";
            break;
        default:
            name = "propertyString";
        }

        Map<String, String> attributes = new HashMap<String, String>();
        attributes.put("propertyDefinitionId", propertyDefinition.getId());
        attributes.put("queryName", propertyDefinition.getQueryName());
        // optional value
        if (propertyDefinition.getDisplayName() !=null && propertyDefinition.getDisplayName().trim().length() > 0)
        {
            attributes.put("displayName", propertyDefinition.getDisplayName());
        }
        // optional value
        if (propertyDefinition.getLocalName() !=null && propertyDefinition.getLocalName().trim().length() > 0)
        {
            attributes.put("localName", propertyDefinition.getLocalName());
        }


        List<CmisExtensionElement> propertyValues = new ArrayList<CmisExtensionElement>();
        if (value != null)
        {
            if (value instanceof List)
            {
                for (Object o : ((List) value))
                {
                	if(o != null)
                	{
                		propertyValues.add(new CmisExtensionElementImpl(CMIS_NAMESPACE, "value", null,
                				convertAspectPropertyValue(o)));
                	}
                	else
                	{
                		logger.warn("Unexpected null entry in list value for property " + propertyDefinition.getDisplayName()
                				+ ", value = " + value);
                	}
                }
            }
            else
            {
                propertyValues.add(new CmisExtensionElementImpl(CMIS_NAMESPACE, "value", null,
                        convertAspectPropertyValue(value)));
            }
        }

        return new CmisExtensionElementImpl(CMIS_NAMESPACE, name, attributes, propertyValues);
    }

    private String convertAspectPropertyValue(Object value)
    {
        if (value instanceof Date)
        {
            GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
            cal.setTime((Date) value);
            value = cal;
        }

        if (value instanceof GregorianCalendar)
        {
            DatatypeFactory df;
            try
            {
                df = DatatypeFactory.newInstance();
            }
            catch (DatatypeConfigurationException e)
            {
                throw new IllegalArgumentException("Aspect conversation exception: " + e.getMessage(), e);
            }
            return df.newXMLGregorianCalendar((GregorianCalendar) value).toXMLFormat();
        }

        // MNT-12496 MNT-15044
        // Filter for AtomPub and Web services bindings only. Browser/json binding already encodes.
        if (AlfrescoCmisServiceCall.get() != null &&
                (CallContext.BINDING_ATOMPUB.equals(AlfrescoCmisServiceCall.get().getBinding()) ||
                 CallContext.BINDING_WEBSERVICES.equals(AlfrescoCmisServiceCall.get().getBinding())))
        {
        	return filterXmlRestrictedCharacters(value.toString());
        }
        else
        {
        	return value.toString();
        }
    }

    @SuppressWarnings("unchecked")
    private AbstractPropertyData<?> getProperty(PropertyType propType, PropertyDefinitionWrapper propDef,
            Serializable value)
    {
        AbstractPropertyData<?> result = null;
        switch (propType)
        {
        case BOOLEAN:
            result = new PropertyBooleanImpl();
            if (value instanceof List)
            {
                ((PropertyBooleanImpl) result).setValues((List<Boolean>) value);
            }
            else
            {
                ((PropertyBooleanImpl) result).setValue((Boolean) value);
            }
            break;
        case DATETIME:
            result = new PropertyDateTimeImpl();
            if (value instanceof List)
            {
                ((PropertyDateTimeImpl) result).setValues((List<GregorianCalendar>) DefaultTypeConverter.INSTANCE
                        .convert(GregorianCalendar.class, (List<?>) value));
            }
            else
            {
                ((PropertyDateTimeImpl) result).setValue(DefaultTypeConverter.INSTANCE.convert(GregorianCalendar.class,
                        value));
            }
            break;
        case DECIMAL:
            result = new PropertyDecimalImpl();
            if (value instanceof List)
            {
                ((PropertyDecimalImpl) result).setValues((List<BigDecimal>) DefaultTypeConverter.INSTANCE.convert(
                        BigDecimal.class, (List<?>) value));
            }
            else
            {
                ((PropertyDecimalImpl) result).setValue(DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, value));
            }
            break;
        case HTML:
            result = new PropertyHtmlImpl();
            if (value instanceof List)
            {
                ((PropertyHtmlImpl) result).setValues((List<String>) value);
            }
            else
            {
                ((PropertyHtmlImpl) result).setValue((String) value);
            }
            break;
        case ID:
            result = new PropertyIdImpl();
            if (value instanceof List)
            {
                ((PropertyIdImpl) result).setValues((List<String>) value);
            }
            else
            {
                if (value instanceof NodeRef)
                {
                    ((PropertyIdImpl) result).setValue(constructObjectId((NodeRef)value, null));
                }
                else
                {
                    ((PropertyIdImpl) result).setValue((String) value);
                }
            }
            break;
        case INTEGER:
            result = new PropertyIntegerImpl();
            if (value instanceof List)
            {
                ((PropertyIntegerImpl) result).setValues((List<BigInteger>) DefaultTypeConverter.INSTANCE.convert(
                        BigInteger.class, (List<?>) value));
            }
            else
            {
                ((PropertyIntegerImpl) result).setValue(DefaultTypeConverter.INSTANCE.convert(BigInteger.class, value));
            }
            break;
        case STRING:
            result = new PropertyStringImpl();
            if (value instanceof List)
            {
                ((PropertyStringImpl) result).setValues((List<String>) value);
            }
            else
            {
               // MNT-12496 MNT-15044
               // Filter for AtomPub and Web services bindings only. Browser/json binding already encodes.
                if (AlfrescoCmisServiceCall.get() != null &&
                        (CallContext.BINDING_ATOMPUB.equals(AlfrescoCmisServiceCall.get().getBinding()) ||
                         CallContext.BINDING_WEBSERVICES.equals(AlfrescoCmisServiceCall.get().getBinding())))
                {
                    ((PropertyStringImpl) result).setValue(filterXmlRestrictedCharacters((String) value));
                }
                else
                {
                    ((PropertyStringImpl) result).setValue((String) value);
                }
            }
            break;
        case URI:
            result = new PropertyUriImpl();
            if (value instanceof List)
            {
                ((PropertyUriImpl) result).setValues((List<String>) value);
            }
            else
            {
                ((PropertyUriImpl) result).setValue((String) value);
            }
            break;
        default:
            throw new RuntimeException("Unknown datatype! Spec change?");
        }

        if (propDef != null)
        {
            result.setId(propDef.getPropertyDefinition().getId());
            result.setQueryName(propDef.getPropertyDefinition().getQueryName());
            result.setDisplayName(propDef.getPropertyDefinition().getDisplayName());
            result.setLocalName(propDef.getPropertyDefinition().getLocalName());
        }

        return result;
    }

    private String filterXmlRestrictedCharacters(String origValue)
    {
        if (origValue == null)
        {
            return origValue;
        }
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < origValue.length(); i++)
        {
            char ch = origValue.charAt(i);
            boolean restricted = (ch < '\u0020') && !(ch == '\t' || ch == '\n' || ch == '\r');
            sb.append(restricted ? REPLACEMENT_CHAR : ch);
        }

        return sb.toString();
    }

    private Set<String> splitFilter(String filter)
    {
        if (filter == null)
        {
            return null;
        }

        if (filter.trim().length() == 0)
        {
            return null;
        }

        Set<String> result = new HashSet<String>();
        for (String s : filter.split(","))
        {
            s = s.trim();
            if (s.equals("*"))
            {
                return null;
            }
            else if (s.length() > 0)
            {
                result.add(s);
            }
        }

        // set a few base properties
        result.add(QUERY_NAME_OBJECT_ID);
        result.add(QUERY_NAME_OBJECT_TYPE_ID);
        result.add(QUERY_NAME_BASE_TYPE_ID);

        return result;
    }

    public AllowableActions getAllowableActions(CMISNodeInfo info)
    {
        AllowableActionsImpl result = new AllowableActionsImpl();
        Set<Action> allowableActions = new HashSet<Action>();
        result.setAllowableActions(allowableActions);

        for (CMISActionEvaluator evaluator : info.getType().getActionEvaluators().values())
        {
            if (evaluator.isAllowed(info))
            {
                allowableActions.add(evaluator.getAction());
            }
        }

        return result;
    }

    public List<ObjectData> getRelationships(NodeRef nodeRef, IncludeRelationships includeRelationships/*, CmisVersion cmisVersion*/)
    {
        List<ObjectData> result = new ArrayList<ObjectData>();

        if (nodeRef.getStoreRef().getProtocol().equals(VersionBaseModel.STORE_PROTOCOL))
        {
            // relationships from and to versions are not preserved
            return result;
        }

        // get relationships
        List<AssociationRef> assocs = new ArrayList<AssociationRef>();
        if (includeRelationships == IncludeRelationships.SOURCE || includeRelationships == IncludeRelationships.BOTH)
        {
            assocs.addAll(nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }
        if (includeRelationships == IncludeRelationships.TARGET || includeRelationships == IncludeRelationships.BOTH)
        {
            assocs.addAll(nodeService.getSourceAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }

        // filter relationships that not map the CMIS domain model
        for (AssociationRef assocRef : assocs)
        {
            TypeDefinitionWrapper assocTypeDef = getOpenCMISDictionaryService().findAssocType(assocRef.getTypeQName());
            if (assocTypeDef == null || getType(assocRef.getSourceRef()) == null
                    || getType(assocRef.getTargetRef()) == null)
            {
                continue;
            }

            try
            {
	            result.add(createCMISObject(createNodeInfo(assocRef), null, false, IncludeRelationships.NONE,
	                    RENDITION_NONE, false, false/*, cmisVersion*/));
            }
            catch(AccessDeniedException e)
            {
            	// PUBLICAPI-110
            	// ok, just skip it
            }
            catch(CmisObjectNotFoundException e)
            {
                // ignore objects that have not been found (perhaps because their type is unknown to CMIS)
            }
            // TODO: Somewhere this has not been wrapped correctly
            catch (net.sf.acegisecurity.AccessDeniedException e)
            {
                // skip
            }
        }

        return result;
    }

    public ObjectList getObjectRelationships(NodeRef nodeRef, RelationshipDirection relationshipDirection,
            String typeId, String filter, Boolean includeAllowableActions, BigInteger maxItems, BigInteger skipCount)
    {
        ObjectListImpl result = new ObjectListImpl();
        result.setHasMoreItems(false);
        result.setNumItems(BigInteger.ZERO);
        result.setObjects(new ArrayList<ObjectData>());

        if (nodeRef.getStoreRef().getProtocol().equals(VersionBaseModel.STORE_PROTOCOL))
        {
            // relationships from and to versions are not preserved
            return result;
        }

        // get relationships
        List<AssociationRef> assocs = new ArrayList<AssociationRef>();
        if (relationshipDirection == RelationshipDirection.SOURCE
                || relationshipDirection == RelationshipDirection.EITHER)
        {
            assocs.addAll(nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }
        if (relationshipDirection == RelationshipDirection.TARGET
                || relationshipDirection == RelationshipDirection.EITHER)
        {
            assocs.addAll(nodeService.getSourceAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }

        int skip = (skipCount == null ? 0 : skipCount.intValue());
        int max = (maxItems == null ? Integer.MAX_VALUE : maxItems.intValue());
        int counter = 0;
        boolean hasMore = false;

        if (max > 0)
        {
            // filter relationships that not map the CMIS domain model
            for (AssociationRef assocRef : assocs)
            {
                TypeDefinitionWrapper assocTypeDef = getOpenCMISDictionaryService().findAssocType(assocRef.getTypeQName());
                if (assocTypeDef == null || getType(assocRef.getSourceRef()) == null
                        || getType(assocRef.getTargetRef()) == null)
                {
                    continue;
                }

                if ((typeId != null) && !assocTypeDef.getTypeId().equals(typeId))
                {
                    continue;
                }

                counter++;

                if (skip > 0)
                {
                    skip--;
                    continue;
                }

                max--;
                if (max > 0)
                {
                	try
                	{
	                    result.getObjects().add(
	                            createCMISObject(createNodeInfo(assocRef), filter, includeAllowableActions,
	                                    IncludeRelationships.NONE, RENDITION_NONE, false, false/*, cmisVersion*/));
                    }
                    catch(CmisObjectNotFoundException e)
                    {
                        // ignore objects that have not been found (perhaps because their type is unknown to CMIS)
                    }
                }
                else
                {
                    hasMore = true;
                }
            }
        }

        result.setNumItems(BigInteger.valueOf(counter));
        result.setHasMoreItems(hasMore);

        return result;
    }

    public List<RenditionData> getRenditions(NodeRef nodeRef, String renditionFilter, BigInteger maxItems,
            BigInteger skipCount)
    {
        CMISRenditionMapping mapping = getRenditionMapping();
        return mapping.getRenditions(nodeRef, renditionFilter, maxItems, skipCount);
    }

    public Acl getACL(NodeRef nodeRef, boolean onlyBasicPermissions)
    {
        AccessControlListImpl result = new AccessControlListImpl();

        // get the permissions and sort them
        ArrayList<AccessPermission> ordered = new ArrayList<AccessPermission>(
                permissionService.getAllSetPermissions(nodeRef));
        Collections.sort(ordered, new AccessPermissionComparator());

        // remove denied permissions and create OpenCMIS objects
        Map<String, Map<Boolean, AccessControlEntryImpl>> aceMap = new HashMap<String, Map<Boolean, AccessControlEntryImpl>>();
        for (AccessPermission entry : ordered)
        {
            if (entry.getAccessStatus() == AccessStatus.ALLOWED)
            {
                // add allowed entries
                Map<Boolean, AccessControlEntryImpl> directAce = aceMap.get(entry.getAuthority());
                if (directAce == null)
                {
                    directAce = new HashMap<Boolean, AccessControlEntryImpl>();
                    aceMap.put(entry.getAuthority(), directAce);
                }

                AccessControlEntryImpl ace = directAce.get(entry.isSetDirectly());
                if (ace == null)
                {
                    ace = new AccessControlEntryImpl();
                    ace.setPrincipal(new AccessControlPrincipalDataImpl(entry.getAuthority()));
                    ace.setPermissions(new ArrayList<String>());
                    ace.setDirect(entry.isSetDirectly());
                    directAce.put(entry.isSetDirectly(), ace);
                }

                ace.getPermissions().add(entry.getPermission());
            }
            else if (entry.getAccessStatus() == AccessStatus.DENIED)
            {
                // remove denied entries
                Map<Boolean, AccessControlEntryImpl> directAce = aceMap.get(entry.getAuthority());
                if (directAce != null)
                {
                    for (AccessControlEntryImpl ace : directAce.values())
                    {
                        ace.getPermissions().remove(entry.getPermission());
                    }
                }
            }
        }

        // adjust permissions, add CMIS permissions and add ACEs to ACL
        List<Ace> aces = new ArrayList<Ace>();
        result.setAces(aces);
        for (Map<Boolean, AccessControlEntryImpl> bothAces : aceMap.values())
        {
            // get, translate and set direct ACE
            AccessControlEntryImpl directAce = bothAces.get(true);
            if ((directAce != null) && (!directAce.getPermissions().isEmpty()))
            {
            	List<String> permissions = translatePermissionsToCMIS(directAce.getPermissions(), onlyBasicPermissions);
            	if(permissions != null && !permissions.isEmpty())
            	{
                	// tck doesn't like empty permissions list
	                directAce.setPermissions(permissions);
	                aces.add(directAce);
            	}
            }

            // get, translate, remove duplicate permissions and set indirect ACE
            AccessControlEntryImpl indirectAce = bothAces.get(false);
            if ((indirectAce != null) && (!indirectAce.getPermissions().isEmpty()))
            {
            	List<String> permissions = translatePermissionsToCMIS(indirectAce.getPermissions(), onlyBasicPermissions);
                indirectAce.setPermissions(permissions);

                // remove permissions that are already set in the direct ACE
                if ((directAce != null) && (!directAce.getPermissions().isEmpty()))
                {
                    indirectAce.getPermissions().removeAll(directAce.getPermissions());
                }

                if(!indirectAce.getPermissions().isEmpty())
                {
                	// tck doesn't like empty permissions list
                	aces.add(indirectAce);
                }
            }
        }

        result.setExact(!onlyBasicPermissions);

        return result;
    }

    private List<String> translatePermissionsToCMIS(List<String> permissions, boolean onlyBasicPermissions)
    {
        Set<String> result = new TreeSet<String>();

        for (String permission : permissions)
        {
            PermissionReference permissionReference = permissionModelDao.getPermissionReference(null, permission);

            if (onlyBasicPermissions)
            {

                // check for full permissions
                if (permissionModelDao.hasFull(permissionReference))
                {
                    result.add(BasicPermissions.ALL);
                }

                // check short forms
                Set<PermissionReference> longForms = permissionModelDao.getGranteePermissions(permissionReference);

                HashSet<String> shortForms = new HashSet<String>();
                for (PermissionReference longForm : longForms)
                {
                    shortForms.add(permissionModelDao.isUnique(longForm) ? longForm.getName() : longForm.toString());
                }

                for (String perm : shortForms)
                {
                    if (PermissionService.READ.equals(perm))
                    {
                        result.add(BasicPermissions.READ);
                    }
                    else if (PermissionService.WRITE.equals(perm))
                    {
                        result.add(BasicPermissions.WRITE);
                    }
                    else if (PermissionService.ALL_PERMISSIONS.equals(perm))
                    {
                        result.add(BasicPermissions.ALL);
                    }
                }

                // check the permission
                if (PermissionService.READ.equals(permission))
                {
                    result.add(BasicPermissions.READ);
                }
                else if (PermissionService.WRITE.equals(permission))
                {
                    result.add(BasicPermissions.WRITE);
                }
                else if (PermissionService.ALL_PERMISSIONS.equals(permission))
                {
                    result.add(BasicPermissions.ALL);
                }
            }
            else
            {
                // ACE-2224: only repository specific permissions should be reported
                if (permission.startsWith("{"))
                {
                    result.add(permission);
                }
                // do revert conversion for basic permissions that have exact matching
                else if (PermissionService.READ.equals(permission))
                {
                    result.add(BasicPermissions.READ);
                }
                else if (PermissionService.WRITE.equals(permission))
                {
                    result.add(BasicPermissions.WRITE);
                }
                else if (PermissionService.ALL_PERMISSIONS.equals(permission))
                {
                    result.add(BasicPermissions.ALL);
                }
                else
                {
                    result.add(permissionReference.toString());
                }
            }
        }

        return new ArrayList<String>(result);
    }

    public static class AccessPermissionComparator implements Comparator<AccessPermission>
    {
        public int compare(AccessPermission left, AccessPermission right)
        {
            if (left.getPosition() != right.getPosition())
            {
                return right.getPosition() - left.getPosition();
            }
            else
            {
                if (left.getAccessStatus() != right.getAccessStatus())
                {
                    return (left.getAccessStatus() == AccessStatus.DENIED) ? -1 : 1;
                }
                else
                {
                    int compare = left.getAuthority().compareTo(right.getAuthority());
                    if (compare != 0)
                    {
                        return compare;
                    }
                    else
                    {
                        return (left.getPermission().compareTo(right.getPermission()));
                    }

                }

            }
        }
    }

    /**
     * Applies the given ACLs.
     */
    public void applyACL(NodeRef nodeRef, TypeDefinitionWrapper type, Acl addAces, Acl removeAces)
    {
        boolean hasAdd = (addAces != null) && (addAces.getAces() != null) && !addAces.getAces().isEmpty();
        boolean hasRemove = (removeAces != null) && (removeAces.getAces() != null) && !removeAces.getAces().isEmpty();

        if (!hasAdd && !hasRemove)
        {
            return;
        }

        if (!type.getTypeDefinition(false).isControllableAcl())
        {
            throw new CmisConstraintException("Object is not ACL controllable!");
        }

        // remove permissions
        if (hasRemove)
        {
            Set<AccessPermission> permissions = permissionService.getAllSetPermissions(nodeRef);

            // get only direct ACE since only those can be removed
            Acl onlyDirectAcl = excludeInheritedAces(nodeRef, removeAces);

            for (Ace ace : onlyDirectAcl.getAces())
            {
                String principalId = ace.getPrincipalId();
                if (CMIS_USER.equals(principalId))
                {
                    principalId = AuthenticationUtil.getFullyAuthenticatedUser();
                }

                for (String permission : translatePermissionsFromCMIS(ace.getPermissions()))
                {

                    AccessPermission toCheck = new AccessPermissionImpl(permission, AccessStatus.ALLOWED, principalId,
                            0);
                    if (!permissions.contains(toCheck))
                    {
                        throw new CmisConstraintException("No matching ACE found to remove!");
                    }

                    permissionService.deletePermission(nodeRef, principalId, permission);
                }
            }
        }

        // add permissions
        if (hasAdd)
        {
            for (Ace ace : addAces.getAces())
            {
                String principalId = ace.getPrincipalId();
                if (CMIS_USER.equals(principalId))
                {
                    principalId = AuthenticationUtil.getFullyAuthenticatedUser();
                }

                for (String permission : translatePermissionsFromCMIS(ace.getPermissions()))
                {
                    permissionService.setPermission(nodeRef, principalId, permission, true);
                }
            }
        }
    }

    /**
     * Converts Acl to map and ignore the indirect ACEs
     *
     * @param acl Acl
     * @return Map
     */
    private Map<String, Set<String>> convertAclToMap(Acl acl)
    {
        Map<String, Set<String>> result = new HashMap<String, Set<String>>();

        if (acl == null || acl.getAces() == null)
        {
            return result;
        }

        for (Ace ace : acl.getAces())
        {
            // don't consider indirect ACEs - we can't change them
            if (!ace.isDirect())
            {
                // ignore
                continue;
            }

            // although a principal must not be null, check it
            if (ace.getPrincipal() == null || ace.getPrincipal().getId() == null)
            {
                // ignore
                continue;
            }

            Set<String> permissions = result.get(ace.getPrincipal().getId());
            if (permissions == null)
            {
                permissions = new HashSet<String>();
                result.put(ace.getPrincipal().getId(), permissions);
            }

            if (ace.getPermissions() != null)
            {
                permissions.addAll(ace.getPermissions());
            }
        }

        return result;
    }

    /**
     * Filter acl to ignore inherited ACEs
     *
     * @param nodeRef NodeRef
     * @param acl Acl
     * @return Acl
     */
    protected Acl excludeInheritedAces(NodeRef nodeRef, Acl acl)
    {

        List<Ace> newAces = new ArrayList<Ace>();
        Acl allACLs = getACL(nodeRef, false);

        Map<String, Set<String>> originalsAcls = convertAclToMap(allACLs);
        Map<String, Set<String>> newAcls = convertAclToMap(acl);

        // iterate through the original ACEs
        for (Map.Entry<String, Set<String>> ace : originalsAcls.entrySet())
        {

            // add permissions
            Set<String> addPermissions = newAcls.get(ace.getKey());
            if (addPermissions != null)
            {
                ace.getValue().addAll(addPermissions);
            }

            // create new ACE
            if (!ace.getValue().isEmpty())
            {
                newAces.add(new AccessControlEntryImpl(new AccessControlPrincipalDataImpl(ace
                        .getKey()), new ArrayList<String>(ace.getValue())));
            }
        }

        return new AccessControlListImpl(newAces);
    }

    /**
     * Sets the given ACL.
     */
    public void applyACL(NodeRef nodeRef, TypeDefinitionWrapper type, Acl aces)
    {
        boolean hasAces = (aces != null) && (aces.getAces() != null) && !aces.getAces().isEmpty();

        if (!hasAces && !permissionService.getInheritParentPermissions(nodeRef))
        {
            return;
        }

        if (!type.getTypeDefinition(false).isControllableAcl())
        {
            throw new CmisConstraintException("Object is not ACL controllable!");
        }

        // remove all permissions
        permissionService.deletePermissions(nodeRef);

        // set new permissions
        for (Ace ace : aces.getAces())
        {
            String principalId = ace.getPrincipalId();
            if (CMIS_USER.equals(principalId))
            {
                principalId = AuthenticationUtil.getFullyAuthenticatedUser();
            }

            List<String> permissions = translatePermissionsFromCMIS(ace.getPermissions());
            for (String permission : permissions)
            {
                permissionService.setPermission(nodeRef, principalId, permission, true);
            }
        }
    }

    private List<String> translatePermissionsFromCMIS(List<String> permissions)
    {
        List<String> result = new ArrayList<String>();

        if (permissions == null)
        {
            return result;
        }

        for (String permission : permissions)
        {
            if (permission == null)
            {
                throw new CmisConstraintException("Invalid null permission!");
            }

            if (BasicPermissions.READ.equals(permission))
            {
                result.add(PermissionService.READ);
            }
            else if (BasicPermissions.WRITE.equals(permission))
            {
                result.add(PermissionService.WRITE);
            }
            else if (BasicPermissions.ALL.equals(permission))
            {
                result.add(PermissionService.ALL_PERMISSIONS);
            }
            else if (!permission.startsWith("{"))
            {
                result.add(permission);
            }
            else
            {
                int sepIndex = permission.lastIndexOf('.');
                if (sepIndex == -1)
                {
                    result.add(permission);
                }
                else
                {
                    result.add(permission.substring(sepIndex + 1));
                }
            }
        }

        return result;
    }

    public void applyPolicies(NodeRef nodeRef, TypeDefinitionWrapper type, List<String> policies)
    {
        if ((policies == null) || (policies.isEmpty()))
        {
            return;
        }

        if (!type.getTypeDefinition(false).isControllablePolicy())
        {
            throw new CmisConstraintException("Object is not policy controllable!");
        }

        // nothing else to do...
    }

    @SuppressWarnings("unchecked")
	public ObjectList query(String statement, Boolean includeAllowableActions,
            IncludeRelationships includeRelationships, String renditionFilter, BigInteger maxItems, BigInteger skipCount/*, CmisVersion cmisVersion*/)
    {
        // prepare results
        ObjectListImpl result = new ObjectListImpl();
        result.setObjects(new ArrayList<ObjectData>());

        // prepare query
        CMISQueryOptions options = new CMISQueryOptions(statement, getRootStoreRef());
        CmisVersion cmisVersion = getRequestCmisVersion();
        options.setCmisVersion(cmisVersion);
        options.setQueryMode(CMISQueryMode.CMS_WITH_ALFRESCO_EXTENSIONS);

        int skip = 0;
        if ((skipCount != null) && (skipCount.intValue() >= 0))
        {
            skip = skipCount.intValue();
            options.setSkipCount(skip);
        }

        if ((maxItems != null) && (maxItems.intValue() >= 0))
        {
            options.setMaxItems(maxItems.intValue());
        }

        boolean fetchObject = includeAllowableActions || (includeRelationships != IncludeRelationships.NONE)
                || (!RENDITION_NONE.equals(renditionFilter));

        // query
        CMISResultSet rs = getOpenCMISQueryService().query(options);
        try
        {
            CMISResultSetColumn[] columns = rs.getMetaData().getColumns();

            for (CMISResultSetRow row : rs)
            {
                NodeRef nodeRef = row.getNodeRef();

                if(!nodeService.exists(nodeRef) || filter(nodeRef))
                {
                    continue;
                }

                TypeDefinitionWrapper type = getType(nodeRef);
                if (type == null)
                {
                    continue;
                }

                ObjectDataImpl hit = new ObjectDataImpl();
                PropertiesImpl properties = new PropertiesImpl();
                hit.setProperties(properties);

                Map<String, Serializable> values = row.getValues();

                for (CMISResultSetColumn column : columns)
                {
                    AbstractPropertyData<?> property = getProperty(column.getCMISDataType(),
                            column.getCMISPropertyDefinition(), values.get(column.getName()));
                    property.setQueryName(column.getName());
                    properties.addProperty(property);
                }

                if (fetchObject)
                {
                    // set allowable actions
                    if (includeAllowableActions)
                    {
                        CMISNodeInfoImpl nodeInfo = createNodeInfo(nodeRef);
                        if(!nodeInfo.getObjectVariant().equals(CMISObjectVariant.NOT_EXISTING))
                        {
                            hit.setAllowableActions(getAllowableActions(nodeInfo));
                        }
                    }

                    // set relationships
                    if (includeRelationships != IncludeRelationships.NONE)
                    {
                        hit.setRelationships(getRelationships(nodeRef, includeRelationships/*, cmisVersion*/));
                    }

                    // set renditions
                    if (!RENDITION_NONE.equals(renditionFilter))
                    {
                        List<RenditionData> renditions = getRenditions(nodeRef, renditionFilter, null, null);
                        if ((renditions != null) && (!renditions.isEmpty()))
                        {
                            hit.setRenditions(renditions);
                        }
                        else
                        {
                        	hit.setRenditions(Collections.EMPTY_LIST);
                        }
                    }
                }

                result.getObjects().add(hit);
            }

            long numberFound = rs.getNumberFound();
            if(numberFound != -1)
            {
            	result.setNumItems(BigInteger.valueOf(numberFound));
            }
            result.setHasMoreItems(rs.hasMore());

        } finally
        {
            rs.close();
        }

        return result;
    }

    /**
     * Sets property values.
     */
    @SuppressWarnings({ "rawtypes" })
    public void setProperties(NodeRef nodeRef, TypeDefinitionWrapper type, Properties properties, String... exclude)
    {
        if (properties == null)
        {
            return;
        }

        Map<String, PropertyData<?>> incomingPropsMap = properties.getProperties();
        if (incomingPropsMap == null)
        {
            return;
        }

        // extract property data into an easier to use form
        Map<String, Pair<TypeDefinitionWrapper, Serializable>> propsMap = new HashMap<String, Pair<TypeDefinitionWrapper, Serializable>>();
        for (String propertyId : incomingPropsMap.keySet())
        {
            PropertyData<?> property = incomingPropsMap.get(propertyId);
            PropertyDefinitionWrapper propDef = type.getPropertyById(property.getId());
            if (propDef == null)
            {
                propDef = getOpenCMISDictionaryService().findProperty(propertyId);
                if (propDef == null)
                {
                    throw new CmisInvalidArgumentException("Property " + property.getId() + " is unknown!");
                }
            }

            Boolean isOnWorkingCopy = checkOutCheckInService.isWorkingCopy(nodeRef);
            Updatability updatability = propDef.getPropertyDefinition().getUpdatability();
            if (!isUpdatable(updatability, isOnWorkingCopy))
            {
                throw new CmisInvalidArgumentException("Property " + propertyId + " is read-only!");
            }

            TypeDefinitionWrapper propType = propDef.getOwningType();
            Serializable value = getValue(property, propDef.getPropertyDefinition().getCardinality() == Cardinality.MULTI);
            Pair<TypeDefinitionWrapper, Serializable> pair = new Pair<TypeDefinitionWrapper, Serializable>(propType, value);
            propsMap.put(propertyId, pair);
        }

        // Need to do deal with secondary types first
        Pair<TypeDefinitionWrapper, Serializable> pair = propsMap.get(PropertyIds.SECONDARY_OBJECT_TYPE_IDS);
        Serializable secondaryTypesProperty = (pair != null ? pair.getSecond() : null);
        if(secondaryTypesProperty != null)
        {
            if (!(secondaryTypesProperty instanceof List))
            {
                throw new CmisInvalidArgumentException("Secondary types must be a list!");
            }
            List secondaryTypes = (List)secondaryTypesProperty;
            if(secondaryTypes != null && secondaryTypes.size() > 0)
            {
                // add/remove secondary types/aspects
                processSecondaryTypes(nodeRef, secondaryTypes, propsMap);
            }
        }

        for (String propertyId : propsMap.keySet())
        {
            if (propertyId.equals(PropertyIds.SECONDARY_OBJECT_TYPE_IDS))
            {
                // already handled above
                continue;
            }

            pair = propsMap.get(propertyId);
            TypeDefinitionWrapper propType = pair.getFirst();
            Serializable value = pair.getSecond();
            if (Arrays.binarySearch(exclude, propertyId) < 0)
            {
                setProperty(nodeRef, propType, propertyId, value);
            }
        }

        List<CmisExtensionElement> extensions = properties.getExtensions();
        if (extensions != null)
        {
        	boolean isNameChanging = properties.getProperties().containsKey(PropertyIds.NAME);

            for (CmisExtensionElement extension : extensions)
            {
                if (ALFRESCO_EXTENSION_NAMESPACE.equals(extension.getNamespace())
                        && SET_ASPECTS.equals(extension.getName()))
                {
                    setAspectProperties(nodeRef, isNameChanging, extension);
                    break;
                }
            }
        }
    }

    @SuppressWarnings("rawtypes")
    private void processSecondaryTypes(NodeRef nodeRef, List secondaryTypes, Map<String, Pair<TypeDefinitionWrapper, Serializable>> propsToAdd)
    {
        // diff existing aspects and secondaryTypes/aspects list
        Set<QName> existingAspects = nodeService.getAspects(nodeRef);
        Set<QName> secondaryTypeAspects = new HashSet<QName>();
        for(Object o : secondaryTypes)
        {
            String secondaryType = (String)o;

            TypeDefinitionWrapper wrapper = getOpenCMISDictionaryService().findType(secondaryType);
            if(wrapper != null)
            {
                QName aspectQName = wrapper.getAlfrescoName();
                secondaryTypeAspects.add(aspectQName);
            }
            else
            {
                throw new CmisInvalidArgumentException("Invalid secondary type id " + secondaryType);
            }
        }

        Set<QName> aspectsToIgnore = new HashSet<>();
        aspectsToIgnore.add(ContentModel.ASPECT_REFERENCEABLE);
        aspectsToIgnore.add(ContentModel.ASPECT_LOCALIZED);
        aspectsToIgnore.add(ContentModel.ASPECT_WORKING_COPY);

        Set<String> namespacesToIgnore = new HashSet<>(singletonList(NamespaceService.SYSTEM_MODEL_1_0_URI));

        // aspects to add == the list of secondary types - existing aspects - ignored aspects
        Set<QName> toAdd = new HashSet<QName>(secondaryTypeAspects);
        toAdd.removeAll(existingAspects);
        toAdd.removeAll(aspectsToIgnore);
        toAdd.removeIf(a -> namespacesToIgnore.contains(a.getNamespaceURI()));

        // aspects to remove == existing aspects - secondary types
        Set<QName> aspectsToRemove = new HashSet<QName>();
        aspectsToRemove.addAll(existingAspects);
        aspectsToRemove.removeAll(aspectsToIgnore);
        Iterator<QName> it = aspectsToRemove.iterator();
        while(it.hasNext())
        {
            QName aspectQName = it.next();
            TypeDefinitionWrapper w = getOpenCMISDictionaryService().findNodeType(aspectQName);
            if(w == null || secondaryTypeAspects.contains(aspectQName) || namespacesToIgnore.contains(aspectQName.getNamespaceURI()))
            {
                // the type is not exposed,
                // or is in the secondary types to set,
                // or is in the set of namespaces to ignore,
                // so remove it from the "to remove" set
                it.remove();
            }
        }

        // first, remove aspects
        for(QName aspectQName : aspectsToRemove)
        {
            nodeService.removeAspect(nodeRef, aspectQName);
        }

        // add aspects and properties
        for(QName aspectQName : toAdd)
        {
            nodeService.addAspect(nodeRef, aspectQName, null);
        }
    }

    public void addSecondaryTypes(NodeRef nodeRef, List<String> secondaryTypes)
    {
    	if(secondaryTypes != null && secondaryTypes.size() > 0)
    	{
	    	for(String secondaryType : secondaryTypes)
	    	{
	            TypeDefinitionWrapper type = getType(secondaryType);
	            if (type == null)
	            {
	                throw new CmisInvalidArgumentException("Invalid secondaryType: " + secondaryType);
	            }
	    		nodeService.addAspect(nodeRef, type.getAlfrescoName(), null);
	    	}
    	}
    }
    
    public void removeSecondaryTypes(NodeRef nodeRef, List<String> secondaryTypes)
    {
    	if(secondaryTypes != null && secondaryTypes.size() > 0)
    	{
	    	for(String secondaryType : secondaryTypes)
	    	{
	            TypeDefinitionWrapper type = getType(secondaryType);
	            if (type == null)
	            {
	                throw new CmisInvalidArgumentException("Invalid secondaryType: " + secondaryType);
	            }
	    		nodeService.removeAspect(nodeRef, type.getAlfrescoName());
	    	}
    	}
    }

    private void setAspectProperties(NodeRef nodeRef, boolean isNameChanging, CmisExtensionElement aspectExtension)
    {
        if (aspectExtension.getChildren() == null)
        {
            return;
        }

        List<String> aspectsToAdd = new ArrayList<String>();
        List<String> aspectsToRemove = new ArrayList<String>();
        Map<QName, List<Serializable>> aspectProperties = new HashMap<QName, List<Serializable>>();

        for (CmisExtensionElement extension : aspectExtension.getChildren())
        {
            if (!ALFRESCO_EXTENSION_NAMESPACE.equals(extension.getNamespace()))
            {
                continue;
            }

            if (ASPECTS_TO_ADD.equals(extension.getName()) && (extension.getValue() != null))
            {
                aspectsToAdd.add(extension.getValue());
            }
            else if (ASPECTS_TO_REMOVE.equals(extension.getName()) && (extension.getValue() != null))
            {
                aspectsToRemove.add(extension.getValue());
            }
            else if (PROPERTIES.equals(extension.getName()) && (extension.getChildren() != null))
            {
                for (CmisExtensionElement property : extension.getChildren())
                {
                    if (!property.getName().startsWith("property"))
                    {
                        continue;
                    }

                    String propertyId = (property.getAttributes() == null ? null : property.getAttributes().get(
                            "propertyDefinitionId"));
                    if ((propertyId == null) || (property.getChildren() == null))
                    {
                        continue;
                    }

                    PropertyType propertyType = PropertyType.STRING;
                    DatatypeFactory df = null;
                    if (property.getName().equals("propertyBoolean"))
                    {
                        propertyType = PropertyType.BOOLEAN;
                    }
                    else if (property.getName().equals("propertyInteger"))
                    {
                        propertyType = PropertyType.INTEGER;
                    }
                    else if (property.getName().equals("propertyDateTime"))
                    {
                        propertyType = PropertyType.DATETIME;
                        try
                        {
                            df = DatatypeFactory.newInstance();
                        }
                        catch (DatatypeConfigurationException e)
                        {
                            throw new CmisRuntimeException("Aspect conversation exception: " + e.getMessage(), e);
                        }
                    }
                    else if (property.getName().equals("propertyDecimal"))
                    {
                        propertyType = PropertyType.DECIMAL;
                    }

                    ArrayList<Serializable> values = new ArrayList<Serializable>();
                    if (property.getChildren() != null)
                    {
//                        try
//                        {
                            for (CmisExtensionElement valueElement : property.getChildren())
                            {
                                if ("value".equals(valueElement.getName()))
                                {
                                    switch (propertyType)
                                    {
                                    case BOOLEAN:
                                    	try
                                    	{
                                    		values.add(Boolean.parseBoolean(valueElement.getValue()));
	                                	}
	                                	catch (Exception e)
	                                	{
	                                		throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
	                                	}
                                        break;
                                    case DATETIME:
                                    	try
                                    	{
	                                        values.add(df.newXMLGregorianCalendar(valueElement.getValue())
	                                                .toGregorianCalendar());
                                    	}
                                    	catch (Exception e)
                                    	{
                                    		throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    	}
                                        break;
                                    case INTEGER:
                                    	BigInteger value = null;
                                    	try
                                    	{
                                    		value = new BigInteger(valueElement.getValue());
                                    	}
                                    	catch (Exception e)
                                    	{
                                    		throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    	}

                                    	// overflow check
                                    	PropertyDefinitionWrapper propDef = getOpenCMISDictionaryService().findProperty(propertyId);
                                        if(propDef == null)
                                        {
                                            throw new CmisInvalidArgumentException("Property " + propertyId + " is unknown!");
                                        }
                                        
                                        QName propertyQName = propDef.getPropertyAccessor().getMappedProperty();
                                        if (propertyQName == null)
                                        {
                                            throw new CmisConstraintException("Unable to set property " + propertyId + "!");
                                        }

                                		org.alfresco.service.cmr.dictionary.PropertyDefinition def = dictionaryService.getProperty(propertyQName);
                                		QName dataDef = def.getDataType().getName();

                                		if (dataDef.equals(DataTypeDefinition.INT) && (value.compareTo(maxInt) > 0 || value.compareTo(minInt) < 0))
                                		{
                                			throw new CmisConstraintException("Value is out of range for property " + propertyId);
                                		}

                                		if (dataDef.equals(DataTypeDefinition.LONG) && (value.compareTo(maxLong) > 0 || value.compareTo(minLong) < 0 ))
                                		{
                                			throw new CmisConstraintException("Value is out of range for property " + propertyId);
                                		}
                                    	
                                        values.add(value);
                                        break;
                                    case DECIMAL:
                                    	try
                                    	{
                                    		values.add(new BigDecimal(valueElement.getValue()));
                                    	}
                                    	catch (Exception e)
                                    	{
                                    		throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    	}
                                        break;
                                    default:
                                        values.add(valueElement.getValue());
                                    }
                                }
                            }
                    }

                    aspectProperties.put(QName.createQName(propertyId, namespaceService), values);
                }
            }
        }

        // remove and add aspects
        String aspectType = null;
        try
        {
            for (String aspect : aspectsToRemove)
            {
                aspectType = aspect;

                TypeDefinitionWrapper type = getType(aspect);
                if (type == null)
                {
                    throw new CmisInvalidArgumentException("Invalid aspect: " + aspectType);
                }

                QName typeName = type.getAlfrescoName();
                // if aspect is hidden aspect, remove only if hidden node is not client controlled
                if(typeName.equals(ContentModel.ASPECT_HIDDEN))
                {
                	if(hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                	{
                		// manipulate hidden aspect only if client controlled
                		nodeService.removeAspect(nodeRef, typeName);	
                	}
                	
                	
                	
//                	if(!isNameChanging && !hiddenAspect.isClientControlled(nodeRef) && !aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
//                	{
//                    	nodeService.removeAspect(nodeRef, typeName);	
//                	}
                }
                else
                {
                	nodeService.removeAspect(nodeRef, typeName);
                }
            }

            for (String aspect : aspectsToAdd)
            {
                aspectType = aspect;

                TypeDefinitionWrapper type = getType(aspect);
                if (type == null)
                {
                    throw new CmisInvalidArgumentException("Invalid aspect: " + aspectType);
                }

                QName typeName = type.getAlfrescoName();
                // if aspect is hidden aspect, remove only if hidden node is not client controlled
                if(typeName.equals(ContentModel.ASPECT_HIDDEN))
                {
                	if(hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                	{
                		// manipulate hidden aspect only if client controlled
                		nodeService.addAspect(nodeRef, type.getAlfrescoName(),
    	                        Collections.<QName, Serializable> emptyMap());	
                	}
                	
//                	if(!isNameChanging && !hiddenAspect.isClientControlled(nodeRef) && !aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
//                	{
//                		nodeService.addAspect(nodeRef, type.getAlfrescoName(),
//    	                        Collections.<QName, Serializable> emptyMap());	
//                	}
                }
                else
                {
                	nodeService.addAspect(nodeRef, type.getAlfrescoName(),
	                        Collections.<QName, Serializable> emptyMap());
                }
            }
        }
        catch (InvalidAspectException e)
        {
            throw new CmisInvalidArgumentException("Invalid aspect: " + aspectType);
        }
        catch (InvalidNodeRefException e)
        {
            throw new CmisInvalidArgumentException("Invalid node: " + nodeRef);
        }

        // set property
        for (Map.Entry<QName, List<Serializable>> property : aspectProperties.entrySet())
        {
        	QName propertyQName = property.getKey();

            if (property.getValue().isEmpty())
            {
                if(HiddenAspect.HIDDEN_PROPERTIES.contains(property.getKey()))
                {
                	if(hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                	{
                		// manipulate hidden aspect property only if client controlled
                        nodeService.removeProperty(nodeRef, propertyQName);
                	}
                }
                else
                {
                	nodeService.removeProperty(nodeRef, property.getKey());	
                }
            }
            else
            {
                if(HiddenAspect.HIDDEN_PROPERTIES.contains(property.getKey()))
                {
                	if(hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                	{
                		// manipulate hidden aspect property only if client controlled
                    	nodeService.setProperty(nodeRef, property.getKey(), property.getValue().size() == 1 ? property
                    			.getValue().get(0) : (Serializable) property.getValue());
                	}
                }
                else
                {
            	Serializable value = (Serializable)property.getValue();
                	nodeService.setProperty(nodeRef, property.getKey(), property.getValue().size() == 1 ? property
                        .getValue().get(0) : value);
                }
            }
        }
    }

    /**
     * Sets a property value.
     */
	public void setProperty(NodeRef nodeRef, TypeDefinitionWrapper type, String propertyId, Serializable value)
    {
        if (propertyId == null)
        {
            throw new CmisInvalidArgumentException("Cannot process not null property!");
        }

        PropertyDefinitionWrapper propDef = type.getPropertyById(propertyId);
        if (propDef == null)
        {
            throw new CmisInvalidArgumentException("Property " + propertyId + " is unknown!");
        }

        Boolean isOnWorkingCopy = checkOutCheckInService.isWorkingCopy(nodeRef);
        Updatability updatability = propDef.getPropertyDefinition().getUpdatability();
        if (!isUpdatable(updatability, isOnWorkingCopy))
        {
            throw new CmisInvalidArgumentException("Property " + propertyId + " is read-only!");
        }

        if(propDef.getPropertyId().equals(PropertyIds.SECONDARY_OBJECT_TYPE_IDS))
        {
            throw new IllegalArgumentException("Cannot process " + PropertyIds.SECONDARY_OBJECT_TYPE_IDS + " in setProperty");
        }
        else
        {
            QName propertyQName = propDef.getPropertyAccessor().getMappedProperty();
            if (propertyQName == null)
            {
                throw new CmisConstraintException("Unable to set property " + propertyId + "!");
            }

            if (propertyId.equals(PropertyIds.NAME))
            {
                if (!(value instanceof String))
                {
                    throw new CmisInvalidArgumentException("Object name must be a string!");
                }

                try
                {
                    String newName = value.toString();
                    // If the node is checked out and the name property is set on the working copy, make sure the new name has the working copy format
                    if (isOnWorkingCopy)
                    {
                        String wcLabel = (String)this.nodeService.getProperty(nodeRef, ContentModel.PROP_WORKING_COPY_LABEL);
                        if (wcLabel == null)
                        {
                            wcLabel = CheckOutCheckInServiceImpl.getWorkingCopyLabel();
                        }
                        if (!newName.contains(wcLabel))
                        {
                            newName = CheckOutCheckInServiceImpl.createWorkingCopyName(newName, wcLabel);
                        }
                    }
                    
                    fileFolderService.rename(nodeRef, newName);
                }
                catch (FileExistsException e)
                {
                    throw new CmisContentAlreadyExistsException("An object with this name already exists!", e);
                }
                catch (FileNotFoundException e)
                {
                    throw new CmisInvalidArgumentException("Object with id " + nodeRef.getId() + " not found!");
                }
            }
            else
        	{
                // overflow check
                if (propDef.getPropertyDefinition().getPropertyType() == PropertyType.INTEGER && value instanceof BigInteger)
                {
                    org.alfresco.service.cmr.dictionary.PropertyDefinition def = dictionaryService.getProperty(propertyQName);
                    QName dataDef = def.getDataType().getName();
                    BigInteger bigValue = (BigInteger) value;

                    if ((bigValue.compareTo(maxInt) > 0 || bigValue.compareTo(minInt) < 0) && dataDef.equals(DataTypeDefinition.INT))
                    {
                        throw new CmisConstraintException("Value is out of range for property " + propertyQName.getLocalName());
                    }

                    if ((bigValue.compareTo(maxLong) > 0 || bigValue.compareTo(minLong) < 0) && dataDef.equals(DataTypeDefinition.LONG))
                    {
                        throw new CmisConstraintException("Value is out of range for property " + propertyQName.getLocalName());
                    }
                }

                nodeService.setProperty(nodeRef, propertyQName, value);
            }
        }
    }

    private Serializable getValue(PropertyData<?> property, boolean isMultiValue)
    {
        if ((property.getValues() == null) || (property.getValues().isEmpty()))
        {
            return null;
        }

        if (isMultiValue)
        {
            return (Serializable) property.getValues();
        }

        return (Serializable) property.getValues().get(0);
    }

    /**
     * Returns content changes.
     */
    public ObjectList getContentChanges(Holder<String> changeLogToken, BigInteger maxItems)
    {
        final ObjectListImpl result = new ObjectListImpl();
        result.setObjects(new ArrayList<ObjectData>());

        // Collect entryIds to use a counter and a way to find the last changeLogToken
        final List<Long> entryIds = new ArrayList<Long>();

        EntryIdCallback changeLogCollectingCallback = new EntryIdCallback(true)
        {
            @Override
            public boolean handleAuditEntry(Long entryId, String user, long time, Map<String, Serializable> values)
            {
                entryIds.add(entryId);
                result.getObjects().addAll(createChangeEvents(time, values));
                return super.handleAuditEntry(entryId, user, time, values);
            }
        };

        Long from = null;
        if ((changeLogToken != null) && (changeLogToken.getValue() != null))
        {
            try
            {
                from = Long.parseLong(changeLogToken.getValue());
            }
            catch (NumberFormatException e)
            {
                throw new CmisInvalidArgumentException("Invalid change log token: " + changeLogToken.getValue());
            }
        }

        AuditQueryParameters params = new AuditQueryParameters();
        params.setApplicationName(CMIS_CHANGELOG_AUDIT_APPLICATION);
        params.setForward(true);
        params.setFromId(from);

        // So we have a BigInteger.  We need to ensure that we cut it down to an integer smaller than Integer.MAX_VALUE
        
        int maxResults = (maxItems == null ? contentChangesDefaultMaxItems : maxItems.intValue());
        maxResults = maxResults < 1 ? contentChangesDefaultMaxItems : maxResults;           // Just a double check of the unbundled contents
        maxResults = maxResults > contentChangesDefaultMaxItems ? contentChangesDefaultMaxItems : maxResults;   // cut it down
        int queryFor = maxResults + 1;                          // Query for 1 more so that we know if there are more results

        auditService.auditQuery(changeLogCollectingCallback, params, queryFor);

        int resultSize = result.getObjects().size();

        // Use the entryIds as a counter is more reliable then the result.getObjects().
        // result.getObjects() can be more or less then the requested maxResults, because it is filtered based on the content.
        boolean hasMoreItems = entryIds.size() >= maxResults;
        result.setHasMoreItems(hasMoreItems);
        // Check if we got more than the client requested
        if (hasMoreItems && resultSize >= maxResults)
        {
            // We are assuming there are there is only one extra document now in line with how it used to behave
            // Remove extra item that was not actually requested
            result.getObjects().remove(resultSize - 1);
            entryIds.remove(resultSize - 1);
        }

        if (changeLogToken != null)
        {
            //Update the changelog after removing the last item if there are more items.
            Long newChangeLogToken = entryIds.isEmpty() ? from : entryIds.get(entryIds.size() - 1);
            changeLogToken.setValue(String.valueOf(newChangeLogToken));
        }

        return result;
    }

    @SuppressWarnings("unchecked")
    private List<ObjectData> createChangeEvents(long time, Map<String, Serializable> values)
    {
        List<ObjectData> result = new ArrayList<ObjectData>();

        if ((values == null) || (values.size() == 0))
        {
            return result;
        }

        GregorianCalendar changeTime = new GregorianCalendar();
        changeTime.setTimeInMillis(time);

        String appPath = "/" + CMIS_CHANGELOG_AUDIT_APPLICATION + "/";

        for (Entry<String, Serializable> entry : values.entrySet())
        {
            if ((entry.getKey() == null) || (!(entry.getValue() instanceof Map)))
            {
                continue;
            }

            String path = entry.getKey();
            if (!path.startsWith(appPath))
            {
                continue;
            }

            ChangeType changeType = null;
            String changePath = path.substring(appPath.length()).toLowerCase();
            for (ChangeType c : ChangeType.values())
            {
                if (changePath.startsWith(c.value().toLowerCase()))
                {
                    changeType = c;
                    break;
                }
            }

            if (changeType == null)
            {
                continue;
            }

            Map<String, Serializable> valueMap = (Map<String, Serializable>) entry.getValue();
            String objectId = (String) valueMap.get(CMISChangeLogDataExtractor.KEY_OBJECT_ID);

            // build object
            ObjectDataImpl object = new ObjectDataImpl();
            result.add(object);

            PropertiesImpl properties = new PropertiesImpl();
            object.setProperties(properties);
            PropertyIdImpl objectIdProperty = new PropertyIdImpl(PropertyIds.OBJECT_ID, objectId);
            properties.addProperty(objectIdProperty);

            ChangeEventInfoDataImpl changeEvent = new ChangeEventInfoDataImpl();
            object.setChangeEventInfo(changeEvent);
            changeEvent.setChangeType(changeType);
            changeEvent.setChangeTime(changeTime);
        }

        return result;
    }

    private class EntryIdCallback implements AuditQueryCallback
    {
        private final boolean valuesRequired;
        private Long entryId;

        public EntryIdCallback(boolean valuesRequired)
        {
            this.valuesRequired = valuesRequired;
        }

        public String getEntryId()
        {
            return entryId == null ? null : entryId.toString();
        }

        public boolean valuesRequired()
        {
            return this.valuesRequired;
        }

        public final boolean handleAuditEntry(Long entryId, String applicationName, String user, long time,
                Map<String, Serializable> values)
        {
            if (applicationName.equals(CMIS_CHANGELOG_AUDIT_APPLICATION))
            {
                return handleAuditEntry(entryId, user, time, values);
            }
            return true;
        }

        public boolean handleAuditEntry(Long entryId, String user, long time, Map<String, Serializable> values)
        {
            this.entryId = entryId;
            return true;
        }

        public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error)
        {
            throw new CmisRuntimeException("Audit entry " + entryId + ": " + errorMsg, error);
        }
    };

    // --------------------------------------------------------------
    // OpenCMIS methods
    // --------------------------------------------------------------

    /**
     * Returns the value of the given property if it exists and is of the
     * correct type.
     */
    public String getStringProperty(Properties properties, String propertyId)
    {
        if ((properties == null) || (properties.getProperties() == null))
        {
            return null;
        }

        PropertyData<?> property = properties.getProperties().get(propertyId);
        if (!(property instanceof PropertyString))
        {
            return null;
        }

        return ((PropertyString) property).getFirstValue();
    }

    /**
     * Returns the value of the given property if it exists and is of the
     * correct type.
     */
    public String getIdProperty(Properties properties, String propertyId)
    {
        if ((properties == null) || (properties.getProperties() == null))
        {
            return null;
        }

        PropertyData<?> property = properties.getProperties().get(propertyId);
        if (!(property instanceof PropertyId))
        {
            return null;
        }

        return ((PropertyId) property).getFirstValue();
    }

    public String getNameProperty(Properties properties, String fallback)
    {
        String name = getStringProperty(properties, PropertyIds.NAME);
        if ((name == null) || (name.trim().length() == 0))
        {
            if (fallback == null)
            {
                throw new CmisInvalidArgumentException("Property " + PropertyIds.NAME + " must be set!");
            }
            else
            {
                name = fallback;
            }
        }

        return name;
    }

    public String getObjectTypeIdProperty(Properties properties)
    {
        String objectTypeId = getIdProperty(properties, PropertyIds.OBJECT_TYPE_ID);
        if ((objectTypeId == null) || (objectTypeId.trim().length() == 0))
        {
            throw new CmisInvalidArgumentException("Property " + PropertyIds.OBJECT_TYPE_ID + " must be set!");
        }

        return objectTypeId;
    }

    public String getSourceIdProperty(Properties properties)
    {
        String id = getIdProperty(properties, PropertyIds.SOURCE_ID);
        if ((id == null) || (id.trim().length() == 0))
        {
            throw new CmisInvalidArgumentException("Property " + PropertyIds.SOURCE_ID + " must be set!");
        }

        return id;
    }

    public String getTargetIdProperty(Properties properties)
    {
        String id = getIdProperty(properties, PropertyIds.TARGET_ID);
        if ((id == null) || (id.trim().length() == 0))
        {
            throw new CmisInvalidArgumentException("Property " + PropertyIds.TARGET_ID + " must be set!");
        }

        return id;
    }

    /**
     * Returns the repository info object.
     */
    public RepositoryInfo getRepositoryInfo(CmisVersion cmisVersion)
    {
        return createRepositoryInfo(cmisVersion);
    }

    /**
     * Returns the repository id.
     */
    public String getRepositoryId()
    {
        return descriptorService.getCurrentRepositoryDescriptor().getId();
    }

    /**
     * Creates the repository info object.
     */
    private RepositoryInfo createRepositoryInfo(CmisVersion cmisVersion)
    {
        Descriptor currentDescriptor = descriptorService.getCurrentRepositoryDescriptor();

        // get change token
        boolean auditEnabled = auditService.isAuditEnabled(CMIS_CHANGELOG_AUDIT_APPLICATION, "/"
                + CMIS_CHANGELOG_AUDIT_APPLICATION);
        String latestChangeLogToken = null;

        if (auditEnabled)
        {
            EntryIdCallback auditQueryCallback = new EntryIdCallback(false);
            AuditQueryParameters params = new AuditQueryParameters();
            params.setApplicationName(CMIS_CHANGELOG_AUDIT_APPLICATION);
            params.setForward(false);
            auditService.auditQuery(auditQueryCallback, params, 1);
            String entryId = auditQueryCallback.getEntryId();
            // MNT-13529
            // add initial change log token
            latestChangeLogToken = entryId == null ? "0" : entryId;
        }

        // compile repository info
        RepositoryInfoImpl ri = new RepositoryInfoImpl();

        ri.setId(currentDescriptor.getId());
        ri.setName(currentDescriptor.getName());
        ri.setDescription(currentDescriptor.getName());
        ri.setVendorName("Alfresco");
        ri.setProductName("Alfresco " + descriptorService.getServerDescriptor().getEdition());
        ri.setProductVersion(currentDescriptor.getVersion());
        NodeRef rootNodeRef = getRootNodeRef();
        ri.setRootFolder(constructObjectId(rootNodeRef, null));
        ri.setCmisVersion(cmisVersion);

        ri.setChangesIncomplete(true);
        ri.setChangesOnType(Arrays.asList(new BaseTypeId[] { BaseTypeId.CMIS_DOCUMENT, BaseTypeId.CMIS_FOLDER }));
        ri.setLatestChangeLogToken(latestChangeLogToken);
        ri.setPrincipalAnonymous(AuthenticationUtil.getGuestUserName());
        ri.setPrincipalAnyone(PermissionService.ALL_AUTHORITIES);

        RepositoryCapabilitiesImpl repCap = new RepositoryCapabilitiesImpl();
        ri.setCapabilities(repCap);

        repCap.setAllVersionsSearchable(false);
        repCap.setCapabilityAcl(CapabilityAcl.MANAGE);
        repCap.setCapabilityChanges(auditEnabled ? CapabilityChanges.OBJECTIDSONLY : CapabilityChanges.NONE);
        repCap.setCapabilityContentStreamUpdates(CapabilityContentStreamUpdates.ANYTIME);
        repCap.setCapabilityJoin(CapabilityJoin.NONE);
        repCap.setCapabilityQuery(CapabilityQuery.BOTHCOMBINED);
        repCap.setCapabilityRendition(CapabilityRenditions.READ);
        repCap.setIsPwcSearchable(false);
        repCap.setIsPwcUpdatable(true);
        repCap.setSupportsGetDescendants(true);
        repCap.setSupportsGetFolderTree(true);
        repCap.setSupportsMultifiling(true);
        repCap.setSupportsUnfiling(false);
        repCap.setSupportsVersionSpecificFiling(false);

        AclCapabilitiesDataImpl aclCap = new AclCapabilitiesDataImpl();
        ri.setAclCapabilities(aclCap);

        aclCap.setAclPropagation(AclPropagation.PROPAGATE);
        aclCap.setSupportedPermissions(SupportedPermissions.BOTH);
        aclCap.setPermissionDefinitionData(repositoryPermissions);
        aclCap.setPermissionMappingData(permissionMappings);

        return ri;
    }

    private List<PermissionDefinition> getRepositoryPermissions()
    {
        ArrayList<PermissionDefinition> result = new ArrayList<PermissionDefinition>();

        Set<PermissionReference> all = permissionModelDao.getAllPermissions();
        for (PermissionReference pr : all)
        {
            result.add(createPermissionDefinition(pr));
        }

        PermissionReference allPermission = permissionModelDao.getPermissionReference(null,
                PermissionService.ALL_PERMISSIONS);
        result.add(createPermissionDefinition(allPermission));

        PermissionDefinitionDataImpl cmisPermission;

        cmisPermission = new PermissionDefinitionDataImpl();
        cmisPermission.setId(BasicPermissions.READ);
        cmisPermission.setDescription("CMIS Read");
        result.add(cmisPermission);

        cmisPermission = new PermissionDefinitionDataImpl();
        cmisPermission.setId(BasicPermissions.WRITE);
        cmisPermission.setDescription("CMIS Write");
        result.add(cmisPermission);

        cmisPermission = new PermissionDefinitionDataImpl();
        cmisPermission.setId(BasicPermissions.ALL);
        cmisPermission.setDescription("CMIS All");
        result.add(cmisPermission);

        return result;
    }

    private PermissionDefinition createPermissionDefinition(PermissionReference pr)
    {
        PermissionDefinitionDataImpl permission = new PermissionDefinitionDataImpl();
        permission.setId(pr.getQName().toString() + "." + pr.getName());
        permission.setDescription(permission.getId());

        return permission;
    }

    private Map<String, PermissionMapping> getPermissionMappings()
    {
        Map<String, PermissionMapping> result = new HashMap<String, PermissionMapping>();

        for (CMISAllowedActionEnum e : EnumSet.allOf(CMISAllowedActionEnum.class))
        {
            for (Map.Entry<String, List<String>> m : e.getPermissionMapping().entrySet())
            {
                PermissionMappingDataImpl mapping = new PermissionMappingDataImpl();
                mapping.setKey(m.getKey());
                mapping.setPermissions(m.getValue());

                result.put(mapping.getKey(), mapping);
            }
        }

        return result;
    }

    /**
     *
     * @deprecated The RenditionService is being replace by the simpler async RenditionService2.
     */
    @Deprecated
    private CMISRenditionMapping getRenditionMapping()
    {
        CMISRenditionMapping renditionMapping = (CMISRenditionMapping)singletonCache.get(KEY_CMIS_RENDITION_MAPPING_NODEREF);
        if (renditionMapping == null)
        {
            renditionMapping = new CMISRenditionMapping(nodeService, contentService, renditionService,
                    transactionService, kindToRenditionNames);
            
            singletonCache.put(KEY_CMIS_RENDITION_MAPPING_NODEREF, renditionMapping);
        }
        return renditionMapping;
    }

    /**
     * Verify if a property is updatable.
     * @param updatability
     * @param isOnWorkingCopy
     * @return
     */
    private boolean isUpdatable(Updatability updatability, Boolean isOnWorkingCopy)
    {
        if ((updatability == Updatability.READONLY)
                || (updatability == Updatability.WHENCHECKEDOUT && !isOnWorkingCopy))
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}
