/*
 * Copyright (C) 2005-2012 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * 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 .
 */
package org.alfresco.opencmis;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import net.sf.acegisecurity.Authentication;
import org.alfresco.cmis.CMISInvalidArgumentException;
import org.alfresco.model.ContentModel;
import org.alfresco.opencmis.dictionary.CMISNodeInfo;
import org.alfresco.opencmis.dictionary.CMISObjectVariant;
import org.alfresco.opencmis.dictionary.FolderTypeDefintionWrapper;
import org.alfresco.opencmis.dictionary.PropertyDefinitionWrapper;
import org.alfresco.opencmis.dictionary.TypeDefinitionWrapper;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.content.encoding.ContentCharsetFinder;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery;
import org.alfresco.repo.search.QueryParameterDefImpl;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.Authorization;
import org.alfresco.repo.version.VersionModel;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.cmr.version.VersionHistory;
import org.alfresco.service.cmr.version.VersionType;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.Pair;
import org.alfresco.util.TempFileProvider;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.Acl;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
import org.apache.chemistry.opencmis.commons.data.ObjectList;
import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
import org.apache.chemistry.opencmis.commons.data.Properties;
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.TypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.ContentStreamAllowed;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
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.CmisStorageException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisStreamNotSupportedException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.TypeDefinitionContainerImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.TypeDefinitionListImpl;
import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl;
import org.apache.chemistry.opencmis.commons.impl.server.RenditionInfoImpl;
import org.apache.chemistry.opencmis.commons.server.CallContext;
import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
import org.apache.chemistry.opencmis.commons.server.RenditionInfo;
import org.apache.chemistry.opencmis.commons.spi.Holder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * OpenCMIS service implementation
 * 
 * @author florian.mueller
 * @author Derek Hulley
 * @since 4.0
 */
public class AlfrescoCmisServiceImpl extends AbstractCmisService implements AlfrescoCmisService
{
    private static Log logger = LogFactory.getLog(AlfrescoCmisService.class);
    private static final QName PARAM_PARENT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "parent");
    private static final QName PARAM_USERNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "username");
    private static final String LUCENE_QUERY_CHECKEDOUT = "+@cm\\:workingCopyOwner:${cm:username}";
    private static final String LUCENE_QUERY_CHECKEDOUT_IN_FOLDER = "+@cm\\:workingCopyOwner:${cm:username} +PARENT:\"${cm:parent}\"";
    private static final String MIN_FILTER = "cmis:name,cmis:baseTypeId,cmis:objectTypeId,"
            + "cmis:createdBy,cmis:creationDate,cmis:lastModifiedBy,cmis:lastModificationDate,"
            + "cmis:contentStreamLength,cmis:contentStreamMimeType,cmis:contentStreamFileName,"
            + "cmis:contentStreamId";
    private CMISConnector connector;
    private CallContext context;
    private UserTransaction txn;
    
    private Map nodeInfoMap;
    private Map objectInfoMap;
    public AlfrescoCmisService(CMISConnector connector)
    {
        this.connector = connector;
        nodeInfoMap = new HashMap();
        objectInfoMap = new HashMap();
    }
    public void beginCall(CallContext context)
    {
        this.context = context;
        AuthenticationUtil.pushAuthentication();
        try
        {
            String currentUser = connector.getAuthenticationService().getCurrentUserName();
            String user = context.getUsername();
            String password = context.getPassword();
            if (currentUser == null)
            {
                Authorization auth = new Authorization(user, password);
                if (auth.isTicket())
                {
                    connector.getAuthenticationService().validate(auth.getTicket());
                } else
                {
                    connector.getAuthenticationService().authenticate(auth.getUserName(), auth.getPasswordCharArray());
                }
            } else if (currentUser.equals(connector.getProxyUser()))
            {
                if (user != null && user.length() > 0)
                {
                    AuthenticationUtil.setFullyAuthenticatedUser(user);
                }
            }
        } catch (AuthenticationException ae)
        {
            throw new CmisPermissionDeniedException(ae.getMessage(), ae);
        }
        // start read-only transaction
        try
        {
            beginReadOnlyTransaction();
        } catch (Exception e)
        {
            AuthenticationUtil.popAuthentication();
            if (e instanceof CmisBaseException)
            {
                throw (CmisBaseException) e;
            } else
            {
                throw new CmisRuntimeException(e.getMessage(), e);
            }
        }
    }
    @Override
    public void close()
    {
        try
        {
            endReadOnlyTransaction();
        } catch (Exception e)
        {
            if (e instanceof CmisBaseException)
            {
                throw (CmisBaseException) e;
            } else
            {
                throw new CmisRuntimeException(e.getMessage(), e);
            }
        } finally
        {
            AuthenticationUtil.popAuthentication();
            context = null;
            nodeInfoMap.clear();
            objectInfoMap.clear();
        }
    }
    /**
     * Called directly before any CMIS method is used
     */
    void beforeCall();
    
    /**
     * Called directly after any CMIS method is used
     */
    void afterCall();
    
    /**
     * Call before the work method and forms the opposite of {@link #close()}.
     * Gathers the type descendants tree.
     */
    private TypeDefinitionContainer getTypesDescendants(int depth, TypeDefinitionWrapper tdw,
            boolean includePropertyDefinitions)
    {
        TypeDefinitionContainerImpl result = new TypeDefinitionContainerImpl();
        result.setTypeDefinition(tdw.getTypeDefinition(includePropertyDefinitions));
        if (depth != 0)
        {
            if (tdw.getChildren() != null)
            {
                result.setChildren(new ArrayList());
                for (TypeDefinitionWrapper tdc : tdw.getChildren())
                {
                    result.getChildren().add(
                            getTypesDescendants(depth < 0 ? -1 : depth - 1, tdc, includePropertyDefinitions));
                }
            }
        }
        return result;
    }
    // --- navigation service ---
    /*
     * Lucene based getChildren - deactivated
     */
    public ObjectInFolderList XgetChildren(String repositoryId, String folderId, String filter, String orderBy,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // convert BigIntegers to int
        int max = (maxItems == null ? Integer.MAX_VALUE : maxItems.intValue());
        int skip = (skipCount == null || skipCount.intValue() < 0 ? 0 : skipCount.intValue());
        ObjectInFolderListImpl result = new ObjectInFolderListImpl();
        List list = new ArrayList();
        result.setObjects(list);
        // get the children references
        NodeRef folderNodeRef = getOrCreateFolderInfo(folderId, "Folder").getNodeRef();
        // lucene part
        QName PARAM_PARENT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "parent");
        DataTypeDefinition nodeRefDataType = connector.getDictionaryService().getDataType(DataTypeDefinition.NODE_REF);
        SearchParameters params = new SearchParameters();
        params.setLanguage(SearchService.LANGUAGE_LUCENE);
        params.addStore(folderNodeRef.getStoreRef());
        QueryParameterDefinition parentDef = new QueryParameterDefImpl(PARAM_PARENT, nodeRefDataType, true,
                folderNodeRef.toString());
        params.addQueryParameterDefinition(parentDef);
        // Build a query for the appropriate types
        StringBuilder query = new StringBuilder(1024).append("+PARENT:\"${cm:parent}\" -ASPECT:\"")
                .append(ContentModel.ASPECT_WORKING_COPY).append("\" +TYPE:(");
        // Include doc type if necessary
        query.append('"').append(ContentModel.TYPE_CONTENT).append('"');
        query.append(" ");
        query.append('"').append(ContentModel.TYPE_FOLDER).append('"');
        // Always exclude system folders
        query.append(") -TYPE:\"").append(ContentModel.TYPE_SYSTEM_FOLDER).append("\"");
        params.setQuery(query.toString());
        // parseOrderBy(orderBy, params);
        ResultSet resultSet = null;
        List childrenList;
        try
        {
            resultSet = connector.getSearchService().query(params);
            childrenList = resultSet.getNodeRefs();
        } finally
        {
            if (resultSet != null)
                resultSet.close();
        }
        if (max > 0)
        {
            int lastIndex = (max + skip > childrenList.size() ? childrenList.size() : max + skip) - 1;
            for (int i = skip; i <= lastIndex; i++)
            {
                NodeRef child = childrenList.get(i);
                CMISNodeInfo ni = createNodeInfo(child);
                // create a child CMIS object
                ObjectData object = connector.createCMISObject(ni, filter, includeAllowableActions,
                        includeRelationships, renditionFilter, false, false);
                ObjectInFolderDataImpl childData = new ObjectInFolderDataImpl();
                childData.setObject(object);
                // include path segment
                if (includePathSegment)
                {
                    childData.setPathSegment(connector.getName(child));
                }
                // add it
                list.add(childData);
            }
        }
        result.setHasMoreItems(childrenList.size() - skip > result.getObjects().size());
        result.setNumItems(BigInteger.valueOf(childrenList.size()));
        return result;
    }
    public ObjectInFolderList getChildren(String repositoryId, String folderId, String filter, String orderBy,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension)
    {
        long start = System.currentTimeMillis();
        checkRepositoryId(repositoryId);
        // convert BigIntegers to int
        int max = (maxItems == null ? Integer.MAX_VALUE : maxItems.intValue());
        int skip = (skipCount == null || skipCount.intValue() < 0 ? 0 : skipCount.intValue());
        ObjectInFolderListImpl result = new ObjectInFolderListImpl();
        List list = new ArrayList();
        result.setObjects(list);
        // get the children references
        NodeRef folderNodeRef = getOrCreateFolderInfo(folderId, "Folder").getNodeRef();
        // convert orderBy to sortProps
        List> sortProps = null;
        if (orderBy != null)
        {
            sortProps = new ArrayList>(1);
            String[] parts = orderBy.split(",");
            int len = parts.length;
            final int origLen = len;
            if (origLen > 0)
            {
                int maxSortProps = GetChildrenCannedQuery.MAX_FILTER_SORT_PROPS;
                if (len > maxSortProps)
                {
                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Too many sort properties in 'orderBy' - ignore those above max (max="
                                + maxSortProps + ",actual=" + len + ")");
                    }
                    len = maxSortProps;
                }
                for (int i = 0; i < len; i++)
                {
                    String[] sort = parts[i].split(" +");
                    if (sort.length > 0)
                    {
                        PropertyDefinitionWrapper propDef = connector.getOpenCMISDictionaryService()
                                .findPropertyByQueryName(sort[0]);
                        if (propDef != null)
                        {
                            QName sortProp = propDef.getPropertyAccessor().getMappedProperty();
                            if (sortProp != null)
                            {
                                boolean sortAsc = (sort.length == 1) || sort[1].equalsIgnoreCase("asc");
                                sortProps.add(new Pair(sortProp, sortAsc));
                            } else
                            {
                                if (logger.isDebugEnabled())
                                {
                                    logger.debug("Ignore sort property '" + sort[0] + " - mapping not found");
                                }
                            }
                        } else
                        {
                            if (logger.isDebugEnabled())
                            {
                                logger.debug("Ignore sort property '" + sort[0] + " - query name not found");
                            }
                        }
                    }
                }
            }
            if (sortProps.size() < origLen)
            {
                logger.warn("Sort properties trimmed - either too many and/or not found: \n" + "   orig:  " + orderBy
                        + "\n" + "   final: " + sortProps);
            }
        }
        PagingRequest pageRequest = new PagingRequest(skip, max, null);
        pageRequest.setRequestTotalCountMax(skip + 10000); // TODO make this
                                                           // optional/configurable
                                                           // - affects whether
                                                           // numItems may be
                                                           // returned
        PagingResults pageOfNodeInfos = connector.getFileFolderService().list(folderNodeRef, true, true,
                null, sortProps, pageRequest);
        if (max > 0)
        {
            for (FileInfo child : pageOfNodeInfos.getPage())
            {
                try
                {
                    // create a child CMIS object
                    CMISNodeInfo ni = createNodeInfo(child.getNodeRef());
                    ObjectData object = connector.createCMISObject(ni, child, filter, includeAllowableActions,
                            includeRelationships, renditionFilter, false, false);
                    if (context.isObjectInfoRequired())
                    {
                        getObjectInfo(repositoryId, ni.getObjectId(), includeRelationships);
                    }
                    ObjectInFolderDataImpl childData = new ObjectInFolderDataImpl();
                    childData.setObject(object);
                    // include path segment
                    if (includePathSegment)
                    {
                        childData.setPathSegment(child.getName());
                    }
                    // add it
                    list.add(childData);
                } catch (InvalidNodeRefException e)
                {
                    // ignore invalid children
                }
            }
        }
        // has more ?
        result.setHasMoreItems(pageOfNodeInfos.hasMoreItems());
        // total count ?
        Pair totalCounts = pageOfNodeInfos.getTotalResultCount();
        if (totalCounts != null)
        {
            Integer totalCountLower = totalCounts.getFirst();
            Integer totalCountUpper = totalCounts.getSecond();
            if ((totalCountLower != null) && (totalCountLower.equals(totalCountUpper)))
            {
                result.setNumItems(BigInteger.valueOf(totalCountLower));
            }
        }
        if (logger.isDebugEnabled())
        {
            logger.debug("getChildren: " + list.size() + " in " + (System.currentTimeMillis() - start) + " msecs");
        }
        return result;
    }
    @Override
    public List getDescendants(String repositoryId, String folderId, BigInteger depth,
            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
            String renditionFilter, Boolean includePathSegment, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        List result = new ArrayList();
        getDescendantsTree(repositoryId, getOrCreateFolderInfo(folderId, "Folder").getNodeRef(), depth.intValue(),
                filter, includeAllowableActions, includeRelationships, renditionFilter, includePathSegment, false,
                result);
        return result;
    }
    @Override
    public List getFolderTree(String repositoryId, String folderId, BigInteger depth,
            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
            String renditionFilter, Boolean includePathSegment, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        List result = new ArrayList();
        getDescendantsTree(repositoryId, getOrCreateFolderInfo(folderId, "Folder").getNodeRef(), depth.intValue(),
                filter, includeAllowableActions, includeRelationships, renditionFilter, includePathSegment, true,
                result);
        return result;
    }
    private void getDescendantsTree(String repositoryId, NodeRef folderNodeRef, int depth, String filter,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includePathSegment, boolean foldersOnly, List list)
    {
        // get the children references
        List childrenList = connector.getNodeService().getChildAssocs(folderNodeRef);
        for (ChildAssociationRef child : childrenList)
        {
            try
            {
                TypeDefinitionWrapper type = connector.getType(child.getChildRef());
                if (type == null)
                {
                    continue;
                }
                boolean isFolder = (type instanceof FolderTypeDefintionWrapper);
                if (foldersOnly && !isFolder)
                {
                    continue;
                }
            	if(connector.isHidden(child.getChildRef()))
            	{
            		continue;
            	}
                // create a child CMIS object
                ObjectInFolderDataImpl object = new ObjectInFolderDataImpl();
                CMISNodeInfo ni = createNodeInfo(child.getChildRef());
                object.setObject(connector.createCMISObject(ni, filter, includeAllowableActions, includeRelationships,
                        renditionFilter, false, false));
                if (context.isObjectInfoRequired())
                {
                    getObjectInfo(repositoryId, ni.getObjectId(), includeRelationships);
                }
                if (includePathSegment)
                {
                    object.setPathSegment(connector.getName(child.getChildRef()));
                }
                // create the container
                ObjectInFolderContainerImpl container = new ObjectInFolderContainerImpl();
                container.setObject(object);
                if ((depth != 1) && isFolder)
                {
                    container.setChildren(new ArrayList());
                    getDescendantsTree(repositoryId, child.getChildRef(), depth - 1, filter, includeAllowableActions,
                            includeRelationships, renditionFilter, includePathSegment, foldersOnly,
                            container.getChildren());
                }
                // add it
                list.add(container);
            } catch (InvalidNodeRefException e)
            {
                // ignore invalid children
            }
        }
    }
    @Override
    public ObjectData getFolderParent(String repositoryId, String folderId, String filter, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // get the node ref
        CMISNodeInfo info = getOrCreateFolderInfo(folderId, "Folder");
        // the root folder has no parent
        if (info.isRootFolder())
        {
            throw new CmisInvalidArgumentException("Root folder has no parent!");
        }
        // get the parent
        List parentInfos = info.getParents();
        if (parentInfos.isEmpty())
        {
            throw new CmisRuntimeException("Folder has no parent and is not the root folder?!");
        }
        CMISNodeInfo parentInfo = addNodeInfo(parentInfos.get(0));
        ObjectData result = connector.createCMISObject(parentInfo, filter, false, IncludeRelationships.NONE,
                CMISConnector.RENDITION_NONE, false, false);
        if (context.isObjectInfoRequired())
        {
            getObjectInfo(repositoryId, parentInfo.getObjectId(), IncludeRelationships.NONE);
        }
        return result;
    }
    @Override
    public List getObjectParents(String repositoryId, String objectId, String filter,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includeRelativePathSegment, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        List result = new ArrayList();
        // what kind of object is it?
        CMISNodeInfo info = getOrCreateNodeInfo(objectId, "Object");
        // relationships are not filed
        if (info.isRelationship())
        {
            throw new CmisConstraintException("Relationships are not fileable!");
        }
        if (info.isFolder() && !info.isRootFolder())
        {
            List parentInfos = info.getParents();
            if (!parentInfos.isEmpty())
            {
                CMISNodeInfo parentInfo = addNodeInfo(parentInfos.get(0));
                ObjectData object = connector.createCMISObject(parentInfo, filter, includeAllowableActions,
                        includeRelationships, renditionFilter, false, false);
                if (context.isObjectInfoRequired())
                {
                    getObjectInfo(repositoryId, object.getId(), includeRelationships);
                }
                ObjectParentDataImpl objectParent = new ObjectParentDataImpl();
                objectParent.setObject(object);
                // include relative path segment
                if (includeRelativePathSegment)
                {
                    objectParent.setRelativePathSegment(info.getName());
                }
                result.add(objectParent);
            }
        } else if (info.isCurrentVersion() || info.isPWC())
        {
            List parentInfos = info.getParents();
            for (CMISNodeInfo parentInfo : parentInfos)
            {
                addNodeInfo(parentInfo);
                ObjectData object = connector.createCMISObject(parentInfo, filter, includeAllowableActions,
                        includeRelationships, renditionFilter, false, false);
                if (context.isObjectInfoRequired())
                {
                    getObjectInfo(repositoryId, object.getId(), includeRelationships);
                }
                ObjectParentDataImpl objectParent = new ObjectParentDataImpl();
                objectParent.setObject(object);
                // include relative path segment
                if (includeRelativePathSegment)
                {
                    objectParent.setRelativePathSegment(info.getName());
                }
                result.add(objectParent);
            }
        }
        return result;
    }
    @Override
    public ObjectList getCheckedOutDocs(String repositoryId, String folderId, String filter, String orderBy,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // convert BigIntegers to int
        int max = (maxItems == null ? Integer.MAX_VALUE : maxItems.intValue());
        int skip = (skipCount == null || skipCount.intValue() < 0 ? 0 : skipCount.intValue());
        // prepare query
        SearchParameters params = new SearchParameters();
        params.setLanguage(SearchService.LANGUAGE_LUCENE);
        QueryParameterDefinition usernameDef = new QueryParameterDefImpl(PARAM_USERNAME, connector
                .getDictionaryService().getDataType(DataTypeDefinition.TEXT), true,
                AuthenticationUtil.getFullyAuthenticatedUser());
        params.addQueryParameterDefinition(usernameDef);
        if (folderId == null)
        {
            params.setQuery(LUCENE_QUERY_CHECKEDOUT);
            params.addStore(connector.getRootStoreRef());
        } else
        {
            CMISNodeInfo folderInfo = getOrCreateFolderInfo(folderId, "Folder");
            params.setQuery(LUCENE_QUERY_CHECKEDOUT_IN_FOLDER);
            params.addStore(folderInfo.getNodeRef().getStoreRef());
            QueryParameterDefinition parentDef = new QueryParameterDefImpl(PARAM_PARENT, connector
                    .getDictionaryService().getDataType(DataTypeDefinition.NODE_REF), true, folderInfo.getNodeRef()
                    .toString());
            params.addQueryParameterDefinition(parentDef);
        }
        // set up order
        if (orderBy != null)
        {
            String[] parts = orderBy.split(",");
            for (int i = 0; i < parts.length; i++)
            {
                String[] sort = parts[i].split(" +");
                if (sort.length < 1)
                {
                    continue;
                }
                PropertyDefinitionWrapper propDef = connector.getOpenCMISDictionaryService().findPropertyByQueryName(
                        sort[0]);
                if (propDef != null)
                {
                    if (propDef.getPropertyDefinition().isOrderable())
                    {
                        QName sortProp = propDef.getPropertyAccessor().getMappedProperty();
                        if (sortProp != null)
                        {
                            boolean sortAsc = (sort.length == 1) || sort[1].equalsIgnoreCase("asc");
                            params.addSort(propDef.getPropertyLuceneBuilder().getLuceneFieldName(), sortAsc);
                        } else
                        {
                            if (logger.isDebugEnabled())
                            {
                                logger.debug("Ignore sort property '" + sort[0] + " - mapping not found");
                            }
                        }
                    } else
                    {
                        if (logger.isDebugEnabled())
                        {
                            logger.debug("Ignore sort property '" + sort[0] + " - not orderable");
                        }
                    }
                } else
                {
                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Ignore sort property '" + sort[0] + " - query name not found");
                    }
                }
            }
        }
        // execute query
        ResultSet resultSet = null;
        List nodeRefs;
        try
        {
            resultSet = connector.getSearchService().query(params);
            nodeRefs = resultSet.getNodeRefs();
        } finally
        {
            if (resultSet != null)
            {
                resultSet.close();
            }
        }
        // collect results
        ObjectListImpl result = new ObjectListImpl();
        List list = new ArrayList();
        result.setObjects(list);
        int skipCounter = skip;
        if (max > 0)
        {
            for (NodeRef nodeRef : nodeRefs)
            {
                if (skipCounter > 0)
                {
                    skipCounter--;
                    continue;
                }
                if (list.size() == max)
                {
                    break;
                }
                try
                {
                    // create a CMIS object
                    CMISNodeInfo ni = createNodeInfo(nodeRef);
                    ObjectData object = connector.createCMISObject(ni, filter, includeAllowableActions,
                            includeRelationships, renditionFilter, false, false);
                    if (context.isObjectInfoRequired())
                    {
                        getObjectInfo(repositoryId, ni.getObjectId(), includeRelationships);
                    }
                    // add it
                    list.add(object);
                } catch (InvalidNodeRefException e)
                {
                    // ignore invalid objects
                }
            }
        }
        // has more ?
        result.setHasMoreItems(nodeRefs.size() - skip > list.size());
        return result;
    }
    // --- object service ---
    @Override
    public String create(String repositoryId, Properties properties, String folderId, ContentStream contentStream,
            VersioningState versioningState, List policies, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // check properties
        if (properties == null || properties.getProperties() == null)
        {
            throw new CmisInvalidArgumentException("Properties must be set!");
        }
        // get the type
        String objectTypeId = connector.getObjectTypeIdProperty(properties);
        // find the type
        TypeDefinitionWrapper type = connector.getOpenCMISDictionaryService().findType(objectTypeId);
        if (type == null)
        {
            throw new CmisInvalidArgumentException("Type '" + objectTypeId + "' is unknown!");
        }
        // create object
        String newId = null;
        switch (type.getBaseTypeId())
        {
        case CMIS_DOCUMENT:
            newId = createDocument(repositoryId, properties, folderId, contentStream, versioningState, policies, null,
                    null, extension);
            break;
        case CMIS_FOLDER:
            newId = createFolder(repositoryId, properties, folderId, policies, null, null, extension);
            break;
        case CMIS_POLICY:
            newId = createPolicy(repositoryId, properties, folderId, policies, null, null, extension);
            break;
        }
        // check new object id
        if (newId == null)
        {
            throw new CmisRuntimeException("Creation failed!");
        }
        if (context.isObjectInfoRequired())
        {
            try
            {
                getObjectInfo(repositoryId, newId, "*", IncludeRelationships.NONE);
            } catch (InvalidNodeRefException e)
            {
                throw new CmisRuntimeException("Creation failed! New object not found!");
            }
        }
        // return the new object id
        return newId;
    }
    @Override
    public String createFolder(String repositoryId, final Properties properties, String folderId,
            final List policies, final Acl addAces, final Acl removeAces, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // get the parent folder node ref
        final CMISNodeInfo parentInfo = getOrCreateFolderInfo(folderId, "Folder");
        // get name and type
        final String name = connector.getNameProperty(properties, null);
        final String objectTypeId = connector.getObjectTypeIdProperty(properties);
        final TypeDefinitionWrapper type = connector.getTypeForCreate(objectTypeId, BaseTypeId.CMIS_FOLDER);
        connector.checkChildObjectType(parentInfo, type.getTypeId());
        // run transaction
        endReadOnlyTransaction();
        NodeRef newNodeRef = connector.getTransactionService().getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionCallback()
                {
                    public NodeRef execute() throws Exception
                    {
                        try
                        {
                            NodeRef nodeRef = connector.getFileFolderService()
                                    .create(parentInfo.getNodeRef(), name, type.getAlfrescoClass()).getNodeRef();
                            connector.setProperties(nodeRef, type, properties, new String[] { PropertyIds.NAME,
                                    PropertyIds.OBJECT_TYPE_ID });
                            connector.applyPolicies(nodeRef, type, policies);
                            connector.applyACL(nodeRef, type, addAces, removeAces);
                            return nodeRef;
                        } catch (FileExistsException fee)
                        {
                            throw new CmisContentAlreadyExistsException("An object with this name already exists!", fee);
                        } catch (IntegrityException ie)
                        {
                            throw new CmisConstraintException("Constraint violation: " + ie.getMessage(), ie);
                        } catch (AccessDeniedException ade)
                        {
                            throw new CmisPermissionDeniedException("Permission denied!", ade);
                        }
                    };
                }, false, true);
        beginReadOnlyTransaction();
        return newNodeRef.getId();
    }
    @Override
    public String createDocument(String repositoryId, final Properties properties, String folderId,
            final ContentStream contentStream, final VersioningState versioningState, final List policies,
            final Acl addAces, final Acl removeAces, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // get the parent folder node ref
        final CMISNodeInfo parentInfo = getOrCreateFolderInfo(folderId, "Parent folder");
        // get name and type
        final String name = connector.getNameProperty(properties, null);
        final String objectTypeId = connector.getObjectTypeIdProperty(properties);
        final TypeDefinitionWrapper type = connector.getTypeForCreate(objectTypeId, BaseTypeId.CMIS_DOCUMENT);
        connector.checkChildObjectType(parentInfo, type.getTypeId());
        DocumentTypeDefinition docType = (DocumentTypeDefinition) type.getTypeDefinition(false);
        if ((docType.getContentStreamAllowed() == ContentStreamAllowed.NOTALLOWED) && (contentStream != null))
        {
            throw new CmisConstraintException("This document type does not support content!");
        }
        if ((docType.getContentStreamAllowed() == ContentStreamAllowed.REQUIRED) && (contentStream == null))
        {
            throw new CmisConstraintException("This document type does requires content!");
        }
        if (docType.isVersionable() && (versioningState == VersioningState.NONE))
        {
            throw new CmisConstraintException("This document type is versionable!");
        }
        if (!docType.isVersionable() && (versioningState != VersioningState.NONE))
        {
            throw new CmisConstraintException("This document type is not versionable!");
        }
        // copy stream to temp file
        final File tempFile = copyToTempFile(contentStream);
        final Charset encoding = (tempFile == null ? null : getEncoding(tempFile, contentStream.getMimeType()));
        // run transaction
        endReadOnlyTransaction();
        NodeRef newNodeRef = connector.getTransactionService().getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionCallback()
                {
                    public NodeRef execute() throws Exception
                    {
                        try
                        {
                            NodeRef nodeRef = connector.getFileFolderService()
                                    .create(parentInfo.getNodeRef(), name, type.getAlfrescoClass()).getNodeRef();
                            connector.setProperties(nodeRef, type, properties, new String[] { PropertyIds.NAME,
                                    PropertyIds.OBJECT_TYPE_ID });
                            connector.applyPolicies(nodeRef, type, policies);
                            connector.applyACL(nodeRef, type, addAces, removeAces);
                            // handle content
                            if (contentStream != null)
                            {
                                // write content
                                ContentWriter writer = connector.getFileFolderService().getWriter(nodeRef);
                                writer.setMimetype(contentStream.getMimeType());
                                writer.setEncoding(encoding.name());
                                writer.putContent(tempFile);
                            }
                            connector.applyVersioningState(nodeRef, versioningState);
                            return nodeRef;
                        } catch (FileExistsException fee)
                        {
                            removeTempFile(tempFile);
                            throw new CmisContentAlreadyExistsException("An object with this name already exists!", fee);
                        } catch (IntegrityException ie)
                        {
                            removeTempFile(tempFile);
                            throw new CmisConstraintException("Constraint violation: " + ie.getMessage(), ie);
                        } catch (AccessDeniedException ade)
                        {
                            removeTempFile(tempFile);
                            throw new CmisPermissionDeniedException("Permission denied!", ade);
                        }
                    };
                }, false, true);
        beginReadOnlyTransaction();
        removeTempFile(tempFile);
        return connector.createObjectId(newNodeRef);
    }
    @Override
    public String createDocumentFromSource(String repositoryId, String sourceId, final Properties properties,
            String folderId, final VersioningState versioningState, final List policies, final Acl addAces,
            final Acl removeAces, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // get the parent folder node ref
        final CMISNodeInfo parentInfo = getOrCreateFolderInfo(folderId, "Parent folder");
        // get source
        CMISNodeInfo info = getOrCreateNodeInfo(sourceId, "Source");
        // check source
        if (info.isVariant(CMISObjectVariant.ASSOC))
        {
            throw new CmisConstraintException("Source object is not a document!");
        }
        final NodeRef sourceNodeRef = info.getNodeRef();
        if (!info.isDocument())
        {
            throw new CmisConstraintException("Source object is not a document!");
        }
        // get name and type
        final String name = connector.getNameProperty(properties, info.getName());
        final TypeDefinitionWrapper type = info.getType();
        connector.checkChildObjectType(parentInfo, type.getTypeId());
        // run transaction
        endReadOnlyTransaction();
        NodeRef newNodeRef = connector.getTransactionService().getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionCallback()
                {
                    public NodeRef execute() throws Exception
                    {
                        try
                        {
                            NodeRef newDocumentNodeRef = connector.getFileFolderService()
                                    .copy(sourceNodeRef, parentInfo.getNodeRef(), name).getNodeRef();
                            connector.setProperties(newDocumentNodeRef, type, properties, new String[] {
                                    PropertyIds.NAME, PropertyIds.OBJECT_TYPE_ID });
                            connector.applyPolicies(newDocumentNodeRef, type, policies);
                            connector.applyACL(newDocumentNodeRef, type, addAces, removeAces);
                            connector.applyVersioningState(newDocumentNodeRef, versioningState);
                            return newDocumentNodeRef;
                        } catch (FileExistsException fee)
                        {
                            throw new CmisContentAlreadyExistsException("An object with this name already exists!", fee);
                        } catch (IntegrityException ie)
                        {
                            throw new CmisConstraintException("Constraint violation: " + ie.getMessage(), ie);
                        } catch (AccessDeniedException ade)
                        {
                            throw new CmisPermissionDeniedException("Permission denied!", ade);
                        }
                    };
                }, false, true);
        beginReadOnlyTransaction();
        return connector.createObjectId(newNodeRef);
    }
    @Override
    public String createPolicy(String repositoryId, Properties properties, String folderId, List policies,
            Acl addAces, Acl removeAces, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // get the parent folder
        getOrCreateFolderInfo(folderId, "Parent Folder");
        String objectTypeId = connector.getObjectTypeIdProperty(properties);
        connector.getTypeForCreate(objectTypeId, BaseTypeId.CMIS_POLICY);
        // we should never get here - policies are not creatable!
        throw new CmisRuntimeException("Polcies cannot be created!");
    }
    @Override
    public String createRelationship(String repositoryId, Properties properties, List policies, Acl addAces,
            Acl removeAces, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        // get type
        String objectTypeId = connector.getObjectTypeIdProperty(properties);
        final TypeDefinitionWrapper type = connector.getTypeForCreate(objectTypeId, BaseTypeId.CMIS_RELATIONSHIP);
        // get source object
        String sourceId = connector.getSourceIdProperty(properties);
        CMISNodeInfo sourceInfo = getOrCreateNodeInfo(sourceId, "Source");
        if (!sourceInfo.isVariant(CMISObjectVariant.CURRENT_VERSION) && !sourceInfo.isVariant(CMISObjectVariant.FOLDER))
        {
            throw new CmisInvalidArgumentException("Source is not the latest version of a document or a folder object!");
        }
        final NodeRef sourceNodeRef = sourceInfo.getNodeRef();
        // get target object
        String targetId = connector.getTargetIdProperty(properties);
        CMISNodeInfo targetInfo = getOrCreateNodeInfo(targetId, "Target");
        if (!targetInfo.isVariant(CMISObjectVariant.CURRENT_VERSION) && !targetInfo.isVariant(CMISObjectVariant.FOLDER))
        {
            throw new CmisInvalidArgumentException(
                    "Target is not the latest version of a document or a folder object!!");
        }
        final NodeRef targetNodeRef = targetInfo.getNodeRef();
        // check policies and ACLs
        if ((policies != null) && (!policies.isEmpty()))
        {
            throw new CmisConstraintException("Relationships are not policy controllable!");
        }
        if ((addAces != null) && (addAces.getAces() != null) && (!addAces.getAces().isEmpty()))
        {
            throw new CmisConstraintException("Relationships are not ACL controllable!");
        }
        if ((removeAces != null) && (removeAces.getAces() != null) && (!removeAces.getAces().isEmpty()))
        {
            throw new CmisConstraintException("Relationships are not ACL controllable!");
        }
        // create relationship
        endReadOnlyTransaction();
        AssociationRef assocRef = connector.getTransactionService().getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionCallback()
                {
                    public AssociationRef execute() throws Exception
                    {
                        try
                        {
                            return connector.getNodeService().createAssociation(sourceNodeRef, targetNodeRef,
                                    type.getAlfrescoClass());
                        } catch (IntegrityException ie)
                        {
                            throw new CmisConstraintException("Constraint violation: " + ie.getMessage(), ie);
                        } catch (AccessDeniedException ade)
                        {
                            throw new CmisPermissionDeniedException("Permission denied!", ade);
                        }
                    };
                }, false, true);
        beginReadOnlyTransaction();
        return CMISConnector.ASSOC_ID_PREFIX + assocRef.getId();
    }
    @Override
    public void setContentStream(String repositoryId, Holder objectId, Boolean overwriteFlag,
            Holder changeToken, final ContentStream contentStream, ExtensionsData extension)
    {
        checkRepositoryId(repositoryId);
        CMISNodeInfo info = getOrCreateNodeInfo(objectId.getValue(), "Object");
        if (!info.isVariant(CMISObjectVariant.CURRENT_VERSION) && !info.isVariant(CMISObjectVariant.PWC))
        {
            throw new CmisStreamNotSupportedException("Content can only be set ondocuments!");
        }
        final NodeRef nodeRef = info.getNodeRef();
        if (((DocumentTypeDefinition) info.getType().getTypeDefinition(false)).getContentStreamAllowed() == ContentStreamAllowed.NOTALLOWED)
        {
            throw new CmisStreamNotSupportedException("Document type doesn't allow content!");
        }
        boolean existed = connector.getContentService().getReader(nodeRef, ContentModel.PROP_CONTENT) != null;
        if (existed && !overwriteFlag)
        {
            throw new CmisContentAlreadyExistsException("Content already exists!");
        }
        if ((contentStream == null) || (contentStream.getStream() == null))
        {
            throw new CmisInvalidArgumentException("No content!");
        }
        // copy stream to temp file
        final File tempFile = copyToTempFile(contentStream);
        final Charset encoding = getEncoding(tempFile, contentStream.getMimeType());
        endReadOnlyTransaction();
        connector.getTransactionService().getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionCallback