001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.opencmis.impl.server;
020
021import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED;
022import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_REMOVED;
023import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_UPDATED;
024import static org.nuxeo.ecm.core.blob.binary.AbstractBinaryManager.MD5_DIGEST;
025import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.CONTENT_MD5_DIGEST_ALGORITHM;
026import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.CONTENT_MD5_HEADER_NAME;
027import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.DIGEST_HEADER_NAME;
028import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoObjectData.REND_STREAM_RENDITION_PREFIX;
029
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.Serializable;
033import java.math.BigInteger;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Calendar;
037import java.util.Collections;
038import java.util.Date;
039import java.util.GregorianCalendar;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Map;
045import java.util.Map.Entry;
046import java.util.Set;
047import java.util.regex.Matcher;
048
049import javax.servlet.http.HttpServletRequest;
050import javax.servlet.http.HttpServletResponse;
051
052import org.apache.chemistry.opencmis.client.api.ObjectId;
053import org.apache.chemistry.opencmis.client.api.OperationContext;
054import org.apache.chemistry.opencmis.client.api.Policy;
055import org.apache.chemistry.opencmis.client.runtime.ObjectIdImpl;
056import org.apache.chemistry.opencmis.commons.BasicPermissions;
057import org.apache.chemistry.opencmis.commons.PropertyIds;
058import org.apache.chemistry.opencmis.commons.data.Ace;
059import org.apache.chemistry.opencmis.commons.data.Acl;
060import org.apache.chemistry.opencmis.commons.data.AllowableActions;
061import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
062import org.apache.chemistry.opencmis.commons.data.ContentStream;
063import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
064import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
065import org.apache.chemistry.opencmis.commons.data.ObjectData;
066import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
067import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
068import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
069import org.apache.chemistry.opencmis.commons.data.ObjectList;
070import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
071import org.apache.chemistry.opencmis.commons.data.Properties;
072import org.apache.chemistry.opencmis.commons.data.PropertyData;
073import org.apache.chemistry.opencmis.commons.data.RenditionData;
074import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
075import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
076import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
077import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
078import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
079import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
080import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
081import org.apache.chemistry.opencmis.commons.enums.Cardinality;
082import org.apache.chemistry.opencmis.commons.enums.ChangeType;
083import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
084import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
085import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
086import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
087import org.apache.chemistry.opencmis.commons.enums.Updatability;
088import org.apache.chemistry.opencmis.commons.enums.VersioningState;
089import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
090import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
091import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
092import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
093import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
094import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
095import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
096import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException;
097import org.apache.chemistry.opencmis.commons.impl.WSConverter;
098import org.apache.chemistry.opencmis.commons.impl.dataobjects.AbstractPropertyData;
099import org.apache.chemistry.opencmis.commons.impl.dataobjects.BindingsObjectFactoryImpl;
100import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateObjectIdAndChangeTokenImpl;
101import org.apache.chemistry.opencmis.commons.impl.dataobjects.ChangeEventInfoDataImpl;
102import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
103import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
104import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
105import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
106import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
107import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
108import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
109import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
110import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
111import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
112import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
113import org.apache.chemistry.opencmis.commons.impl.jaxb.CmisTypeContainer;
114import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
115import org.apache.chemistry.opencmis.commons.server.CallContext;
116import org.apache.chemistry.opencmis.commons.server.CmisService;
117import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
118import org.apache.chemistry.opencmis.commons.server.ProgressControlCmisService;
119import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory;
120import org.apache.chemistry.opencmis.commons.spi.Holder;
121import org.apache.chemistry.opencmis.server.support.wrapper.AbstractCmisServiceWrapper;
122import org.apache.chemistry.opencmis.server.support.wrapper.CallContextAwareCmisService;
123import org.apache.commons.lang3.StringUtils;
124import org.apache.commons.logging.Log;
125import org.apache.commons.logging.LogFactory;
126import org.elasticsearch.action.search.SearchResponse;
127import org.elasticsearch.search.SearchHits;
128import org.nuxeo.common.utils.Path;
129import org.nuxeo.ecm.core.api.Blob;
130import org.nuxeo.ecm.core.api.Blobs;
131import org.nuxeo.ecm.core.api.CloseableCoreSession;
132import org.nuxeo.ecm.core.api.ConcurrentUpdateException;
133import org.nuxeo.ecm.core.api.CoreInstance;
134import org.nuxeo.ecm.core.api.CoreSession;
135import org.nuxeo.ecm.core.api.DocumentModel;
136import org.nuxeo.ecm.core.api.DocumentModelList;
137import org.nuxeo.ecm.core.api.DocumentRef;
138import org.nuxeo.ecm.core.api.Filter;
139import org.nuxeo.ecm.core.api.IdRef;
140import org.nuxeo.ecm.core.api.IterableQueryResult;
141import org.nuxeo.ecm.core.api.LifeCycleConstants;
142import org.nuxeo.ecm.core.api.NuxeoException;
143import org.nuxeo.ecm.core.api.PartialList;
144import org.nuxeo.ecm.core.api.PathRef;
145import org.nuxeo.ecm.core.api.PropertyException;
146import org.nuxeo.ecm.core.api.VersioningOption;
147import org.nuxeo.ecm.core.api.impl.CompoundFilter;
148import org.nuxeo.ecm.core.api.impl.FacetFilter;
149import org.nuxeo.ecm.core.api.impl.LifeCycleFilter;
150import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
151import org.nuxeo.ecm.core.api.security.ACE;
152import org.nuxeo.ecm.core.api.security.ACL;
153import org.nuxeo.ecm.core.api.security.ACP;
154import org.nuxeo.ecm.core.api.security.SecurityConstants;
155import org.nuxeo.ecm.core.opencmis.impl.server.versioning.CMISVersioningFilter;
156import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils;
157import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils.BatchedList;
158import org.nuxeo.ecm.core.opencmis.impl.util.SimpleImageInfo;
159import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
160import org.nuxeo.ecm.core.query.QueryParseException;
161import org.nuxeo.ecm.core.query.sql.NXQL;
162import org.nuxeo.ecm.core.schema.FacetNames;
163import org.nuxeo.ecm.core.security.SecurityService;
164import org.nuxeo.ecm.core.storage.sql.coremodel.SQLDocumentVersion.VersionNotModifiableException;
165import org.nuxeo.ecm.platform.audit.api.AuditReader;
166import org.nuxeo.ecm.platform.audit.api.LogEntry;
167import org.nuxeo.ecm.platform.filemanager.api.FileManager;
168import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
169import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
170import org.nuxeo.ecm.platform.mimetype.service.MimetypeRegistryService;
171import org.nuxeo.ecm.platform.rendition.Rendition;
172import org.nuxeo.ecm.platform.rendition.service.RenditionService;
173import org.nuxeo.elasticsearch.api.ElasticSearchService;
174import org.nuxeo.elasticsearch.api.EsIterableQueryResultImpl;
175import org.nuxeo.elasticsearch.core.EsSearchHitConverter;
176import org.nuxeo.elasticsearch.query.NxQueryBuilder;
177import org.nuxeo.runtime.api.Framework;
178import org.nuxeo.runtime.services.config.ConfigurationService;
179import org.nuxeo.runtime.transaction.TransactionHelper;
180
181/**
182 * Nuxeo implementation of the CMIS Services, on top of a {@link CoreSession}.
183 */
184public class NuxeoCmisService extends AbstractCmisService
185        implements CallContextAwareCmisService, ProgressControlCmisService {
186
187    public static final int DEFAULT_TYPE_LEVELS = 2;
188
189    public static final int DEFAULT_FOLDER_LEVELS = 2;
190
191    public static final int DEFAULT_CHANGE_LOG_SIZE = 100;
192
193    public static final int MAX_CHANGE_LOG_SIZE = 1000 * 1000;
194
195    public static final int DEFAULT_QUERY_SIZE = 100;
196
197    public static final int DEFAULT_MAX_CHILDREN = 100;
198
199    public static final int DEFAULT_MAX_RELATIONSHIPS = 100;
200
201    public static final String PERMISSION_NOTHING = "Nothing";
202
203    /** Synthetic property for change log entries recording the log entry id. */
204    public static final String NX_CHANGE_LOG_ID = "nuxeo:changeLogId";
205
206    public static final String ES_AUDIT_ID = "id";
207
208    public static final String ES_AUDIT_REPOSITORY_ID = "repositoryId";
209
210    public static final String ES_AUDIT_EVENT_ID = "eventId";
211
212    public static final String ERROR_ON_CANCEL_CHECK_OUT_OF_DRAFT_VERSION_PROP = "org.nuxeo.cmis.errorOnCancelCheckOutOfDraftVersion";
213
214    private static final Log log = LogFactory.getLog(NuxeoCmisService.class);
215
216    protected final BindingsObjectFactory objectFactory = new BindingsObjectFactoryImpl();
217
218    protected final NuxeoRepository repository;
219
220    /** When false, we don't own the core session and shouldn't close it. */
221    protected final boolean coreSessionOwned;
222
223    protected CoreSession coreSession;
224
225    /* To avoid refetching it several times per session. */
226    protected String cachedChangeLogToken;
227
228    protected CallContext callContext;
229
230    /** Filter that hides HiddenInNavigation and deleted objects. */
231    protected final Filter documentFilter;
232
233    protected final Set<String> readPermissions;
234
235    protected final Set<String> writePermissions;
236
237    protected final boolean errorOnCancelCheckOutOfDraftVersion;
238
239    public static NuxeoCmisService extractFromCmisService(CmisService service) {
240        if (service == null) {
241            throw new NullPointerException();
242        }
243        for (;;) {
244            if (service instanceof NuxeoCmisService) {
245                return (NuxeoCmisService) service;
246            }
247            if (!(service instanceof AbstractCmisServiceWrapper)) {
248                return null;
249            }
250            service = ((AbstractCmisServiceWrapper) service).getWrappedService();
251        }
252    }
253
254    /**
255     * Constructs a Nuxeo CMIS Service from an existing {@link CoreSession}.
256     *
257     * @param coreSession the session
258     * @since 6.0
259     */
260    public NuxeoCmisService(CoreSession coreSession) {
261        this(coreSession, coreSession.getRepositoryName());
262    }
263
264    /**
265     * Constructs a Nuxeo CMIS Service.
266     *
267     * @param repositoryName the repository name
268     * @since 6.0
269     */
270    public NuxeoCmisService(String repositoryName) {
271        this(null, repositoryName);
272    }
273
274    protected NuxeoCmisService(CoreSession coreSession, String repositoryName) {
275        this.coreSession = coreSession;
276        coreSessionOwned = coreSession == null;
277        repository = getNuxeoRepository(repositoryName);
278        documentFilter = getDocumentFilter();
279        SecurityService securityService = Framework.getService(SecurityService.class);
280        readPermissions = new HashSet<>(Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.READ)));
281        writePermissions = new HashSet<>(
282                Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.READ_WRITE)));
283        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
284        errorOnCancelCheckOutOfDraftVersion = configurationService.isBooleanPropertyTrue(
285                ERROR_ON_CANCEL_CHECK_OUT_OF_DRAFT_VERSION_PROP);
286        CMISVersioningFilter.enable();
287    }
288
289    // called in a finally block from dispatcher
290    @Override
291    public void close() {
292        if (coreSessionOwned && coreSession != null) {
293            ((CloseableCoreSession) coreSession).close();
294            coreSession = null;
295        }
296        clearObjectInfos();
297        CMISVersioningFilter.disable();
298    }
299
300    @Override
301    public Progress beforeServiceCall() {
302        return Progress.CONTINUE;
303    }
304
305    @Override
306    public Progress afterServiceCall() {
307        // check if there is a transaction timeout
308        // if yes, abort and return a 503 (Service Unavailable)
309        if (!TransactionHelper.setTransactionRollbackOnlyIfTimedOut()) {
310            return Progress.CONTINUE;
311        }
312        HttpServletResponse response = (HttpServletResponse) getCallContext().get(CallContext.HTTP_SERVLET_RESPONSE);
313        if (response != null) {
314            try {
315                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Transaction timeout");
316            } catch (IOException e) {
317                throw new CmisRuntimeException("Failed to set timeout status", e);
318            }
319        }
320        return Progress.STOP;
321    }
322
323    protected static NuxeoRepository getNuxeoRepository(String repositoryName) {
324        if (repositoryName == null) {
325            return null;
326        }
327        return Framework.getService(NuxeoRepositories.class).getRepository(repositoryName);
328    }
329
330    protected static CoreSession openCoreSession(String repositoryName, String username) {
331        if (repositoryName == null) {
332            return null;
333        }
334        return CoreInstance.openCoreSession(repositoryName, username);
335    }
336
337    public NuxeoRepository getNuxeoRepository() {
338        return repository;
339    }
340
341    public CoreSession getCoreSession() {
342        return coreSession;
343    }
344
345    public BindingsObjectFactory getObjectFactory() {
346        return objectFactory;
347    }
348
349    @Override
350    public CallContext getCallContext() {
351        return callContext;
352    }
353
354    protected TypeManagerImpl getTypeManager() {
355        CmisVersion cmisVersion = callContext == null ? CmisVersion.CMIS_1_1 : callContext.getCmisVersion();
356        return repository.getTypeManager(cmisVersion);
357    }
358
359    @Override
360    public void setCallContext(CallContext callContext) {
361        close();
362        this.callContext = callContext;
363        if (coreSessionOwned) {
364            // for non-local binding, the principal is found
365            // in the login stack
366            String username = callContext.getBinding().equals(CallContext.BINDING_LOCAL) ? callContext.getUsername()
367                    : null;
368            coreSession = repository == null ? null : openCoreSession(repository.getId(), username);
369            // re-set CMIS automatic versioning filter as it was disabled at close
370            CMISVersioningFilter.enable();
371        }
372    }
373
374    /** Gets the filter that hides HiddenInNavigation and deleted objects. */
375    protected Filter getDocumentFilter() {
376        Filter facetFilter = new FacetFilter(FacetNames.HIDDEN_IN_NAVIGATION, false);
377        Filter lcFilter = new LifeCycleFilter(LifeCycleConstants.DELETED_STATE, false);
378        return new CompoundFilter(facetFilter, lcFilter);
379    }
380
381    protected String getIdFromDocumentRef(DocumentRef ref) {
382        if (ref instanceof IdRef) {
383            return ((IdRef) ref).value;
384        } else {
385            return coreSession.getDocument(ref).getId();
386        }
387    }
388
389    protected void save() {
390        coreSession.save();
391        cachedChangeLogToken = null;
392    }
393
394    /* This is the only method that does not have a repositoryId / coreSession. */
395    @Override
396    public List<RepositoryInfo> getRepositoryInfos(ExtensionsData extension) {
397        List<NuxeoRepository> repos = Framework.getService(NuxeoRepositories.class).getRepositories();
398        List<RepositoryInfo> infos = new ArrayList<>(repos.size());
399        for (NuxeoRepository repo : repos) {
400            String latestChangeLogToken = getLatestChangeLogToken(repo.getId());
401            infos.add(repo.getRepositoryInfo(latestChangeLogToken, callContext));
402        }
403        return infos;
404    }
405
406    @Override
407    public RepositoryInfo getRepositoryInfo(String repositoryId, ExtensionsData extension) {
408        String latestChangeLogToken;
409        if (cachedChangeLogToken != null) {
410            latestChangeLogToken = cachedChangeLogToken;
411        } else {
412            latestChangeLogToken = getLatestChangeLogToken(repositoryId);
413            cachedChangeLogToken = latestChangeLogToken;
414        }
415        NuxeoRepository repository = getNuxeoRepository(repositoryId);
416        return repository.getRepositoryInfo(latestChangeLogToken, callContext);
417    }
418
419    @Override
420    public TypeDefinition getTypeDefinition(String repositoryId, String typeId, ExtensionsData extension) {
421        TypeDefinition type = getTypeManager().getTypeDefinition(typeId);
422        if (type == null) {
423            throw new CmisInvalidArgumentException("No such type: " + typeId);
424        }
425        // TODO copy only when local binding
426        // clone
427        return WSConverter.convert(WSConverter.convert(type));
428
429    }
430
431    @Override
432    public TypeDefinitionList getTypeChildren(String repositoryId, String typeId, Boolean includePropertyDefinitions,
433            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
434        TypeDefinitionList types = getTypeManager().getTypeChildren(typeId, includePropertyDefinitions, maxItems,
435                skipCount);
436        // TODO copy only when local binding
437        // clone
438        return WSConverter.convert(WSConverter.convert(types));
439    }
440
441    @Override
442    public List<TypeDefinitionContainer> getTypeDescendants(String repositoryId, String typeId, BigInteger depth,
443            Boolean includePropertyDefinitions, ExtensionsData extension) {
444        int d = depth == null ? DEFAULT_TYPE_LEVELS : depth.intValue();
445        List<TypeDefinitionContainer> types = getTypeManager().getTypeDescendants(typeId, d,
446                includePropertyDefinitions);
447        // clone
448        // TODO copy only when local binding
449        List<CmisTypeContainer> tmp = new ArrayList<>(types.size());
450        WSConverter.convertTypeContainerList(types, tmp);
451        return WSConverter.convertTypeContainerList(tmp);
452    }
453
454    protected DocumentModel getDocumentModel(String id) {
455        DocumentRef docRef = new IdRef(id);
456        if (!coreSession.exists(docRef)) {
457            throw new CmisObjectNotFoundException(docRef.toString());
458        }
459        DocumentModel doc = coreSession.getDocument(docRef);
460        if (isFilteredOut(doc)) {
461            throw new CmisObjectNotFoundException(docRef.toString());
462        }
463        return doc;
464    }
465
466    @Override
467    public NuxeoObjectData getObject(String repositoryId, String objectId, String filter,
468            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
469            Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
470        DocumentModel doc = getDocumentModel(objectId);
471        NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
472                renditionFilter, includePolicyIds, includeAcl, extension);
473        collectObjectInfo(repositoryId, objectId);
474        return data;
475    }
476
477    /**
478     * Checks if the doc should be ignored because it is "invisible" (deleted, hidden in navigation).
479     */
480    public boolean isFilteredOut(DocumentModel doc) {
481        // don't filter out relations even though they may be HiddenInNavigation
482        if (NuxeoTypeHelper.getBaseTypeId(doc).equals(BaseTypeId.CMIS_RELATIONSHIP)) {
483            return false;
484        }
485        return !documentFilter.accept(doc);
486    }
487
488    /** Creates bare unsaved document model. */
489    protected DocumentModel createDocumentModel(ObjectId folder, TypeDefinition type) {
490        DocumentModel doc;
491        String typeId = type.getId();
492        String nuxeoTypeId = type.getLocalName();
493        if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
494            nuxeoTypeId = NuxeoTypeHelper.NUXEO_FILE;
495        } else if (BaseTypeId.CMIS_FOLDER.value().equals(typeId)) {
496            nuxeoTypeId = NuxeoTypeHelper.NUXEO_FOLDER;
497        } else if (BaseTypeId.CMIS_RELATIONSHIP.value().equals(typeId)) {
498            nuxeoTypeId = NuxeoTypeHelper.NUXEO_RELATION_DEFAULT;
499        }
500        doc = coreSession.createDocumentModel(nuxeoTypeId);
501        if (folder != null) {
502            DocumentRef parentRef = new IdRef(folder.getId());
503            if (!coreSession.exists(parentRef)) {
504                throw new CmisInvalidArgumentException(parentRef.toString());
505            }
506            DocumentModel parentDoc = coreSession.getDocument(parentRef);
507            String pathSegment = nuxeoTypeId; // default path segment based on id
508            doc.setPathInfo(parentDoc.getPathAsString(), pathSegment);
509        }
510        return doc;
511    }
512
513    /** Creates and save document model. */
514    protected DocumentModel createDocumentModel(ObjectId folder, ContentStream contentStream, String name) {
515        FileManager fileManager = Framework.getService(FileManager.class);
516        MimetypeRegistryService mtr = (MimetypeRegistryService) Framework.getService(MimetypeRegistry.class);
517        if (fileManager == null || mtr == null || name == null || folder == null) {
518            return null;
519        }
520
521        DocumentModel parent = coreSession.getDocument(new IdRef(folder.getId()));
522        String path = parent.getPathAsString();
523
524        Blob blob;
525        if (contentStream == null) {
526            String mimeType;
527            try {
528                mimeType = mtr.getMimetypeFromFilename(name);
529            } catch (MimetypeNotFoundException e) {
530                mimeType = MimetypeRegistry.DEFAULT_MIMETYPE;
531            }
532            blob = Blobs.createBlob("", mimeType, null, name);
533        } else {
534            try {
535                blob = NuxeoPropertyData.getPersistentBlob(contentStream, null);
536            } catch (IOException e) {
537                throw new CmisRuntimeException(e.toString(), e);
538            }
539        }
540
541        try {
542            return fileManager.createDocumentFromBlob(coreSession, blob, path, false, name);
543        } catch (IOException e) {
544            throw new CmisRuntimeException(e.toString(), e);
545        }
546    }
547
548    // create and save session
549    protected NuxeoObjectData createObject(String repositoryId, Properties properties, ObjectId folder,
550            BaseTypeId baseType, ContentStream contentStream) {
551        String typeId;
552        Map<String, PropertyData<?>> p;
553        PropertyData<?> d;
554        TypeDefinition type = null;
555        if (properties != null //
556                && (p = properties.getProperties()) != null //
557                && (d = p.get(PropertyIds.OBJECT_TYPE_ID)) != null) {
558            typeId = (String) d.getFirstValue();
559            if (baseType == null) {
560                type = getTypeManager().getTypeDefinition(typeId);
561                if (type == null) {
562                    throw new IllegalArgumentException(typeId);
563                }
564                baseType = type.getBaseTypeId();
565            }
566        } else {
567            typeId = null;
568        }
569        if (typeId == null) {
570            switch (baseType) {
571            case CMIS_DOCUMENT:
572                typeId = BaseTypeId.CMIS_DOCUMENT.value();
573                break;
574            case CMIS_FOLDER:
575                typeId = BaseTypeId.CMIS_FOLDER.value();
576                break;
577            case CMIS_POLICY:
578                throw new CmisRuntimeException("Cannot create policy");
579            case CMIS_RELATIONSHIP:
580                throw new CmisRuntimeException("Cannot create relationship");
581            default:
582                throw new CmisRuntimeException("No base type");
583            }
584        }
585        if (type == null) {
586            type = getTypeManager().getTypeDefinition(typeId);
587        }
588        if (type == null || type.getBaseTypeId() != baseType) {
589            throw new CmisInvalidArgumentException(typeId);
590        }
591        if (type.isCreatable() == Boolean.FALSE) {
592            throw new CmisInvalidArgumentException("Not creatable: " + typeId);
593        }
594
595        // name from properties
596        PropertyData<?> npd = properties.getProperties().get(PropertyIds.NAME);
597        String name = npd == null ? null : (String) npd.getFirstValue();
598        if (StringUtils.isBlank(name)) {
599            throw new CmisConstraintException("The mandatory property " + PropertyIds.NAME + " is missing");
600        }
601
602        // content stream filename default
603        if (contentStream != null && StringUtils.isBlank(contentStream.getFileName())) {
604            // infer filename from name property
605            contentStream = new ContentStreamImpl(name, contentStream.getBigLength(),
606                    contentStream.getMimeType().trim(), contentStream.getStream());
607        }
608
609        DocumentModel doc = null;
610        if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
611            doc = createDocumentModel(folder, contentStream, name);
612        }
613        boolean created = doc != null;
614        if (!created) {
615            doc = createDocumentModel(folder, type);
616        }
617
618        NuxeoObjectData data = new NuxeoObjectData(this, doc);
619        updateProperties(data, properties, true);
620        boolean setContentStream = !created && contentStream != null;
621        if (setContentStream) {
622            try {
623                NuxeoPropertyData.setContentStream(doc, contentStream, true);
624            } catch (CmisContentAlreadyExistsException e) {
625                // cannot happen, overwrite = true
626            } catch (IOException e) {
627                throw new CmisRuntimeException(e.toString(), e);
628            }
629        }
630        if (!created) {
631            // set path segment from properties (name/title)
632            PathSegmentService pss = Framework.getService(PathSegmentService.class);
633            String pathSegment = pss.generatePathSegment(doc);
634            Path path = doc.getPath();
635            doc.setPathInfo(path == null ? null : path.removeLastSegments(1).toString(), pathSegment);
636            doc = coreSession.createDocument(doc);
637        } else {
638            doc = coreSession.saveDocument(doc);
639        }
640        if (setContentStream) {
641            NuxeoPropertyData.validateBlobDigest(doc, callContext);
642        }
643        data.doc = doc;
644        save();
645        collectObjectInfo(repositoryId, data.getId());
646        return data;
647    }
648
649    protected <T> void updateProperties(NuxeoObjectData object, Properties properties, boolean creation) {
650        List<TypeDefinition> types = object.getTypeDefinitions();
651        Map<String, PropertyData<?>> p;
652        if (properties == null || (p = properties.getProperties()) == null) {
653            return;
654        }
655        for (Entry<String, PropertyData<?>> en : p.entrySet()) {
656            String key = en.getKey();
657            PropertyData<?> d = en.getValue();
658            setObjectProperty(object, key, d, types, creation);
659        }
660    }
661
662    protected <T> void updateProperties(NuxeoObjectData object, Map<String, ?> properties, TypeDefinition type,
663            boolean creation) {
664        if (properties == null) {
665            return;
666        }
667        for (Entry<String, ?> en : properties.entrySet()) {
668            String key = en.getKey();
669            Object value = en.getValue();
670            @SuppressWarnings("unchecked")
671            PropertyDefinition<T> pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
672            if (pd == null) {
673                throw new CmisRuntimeException("Unknown property: " + key);
674            }
675            setObjectProperty(object, key, value, pd, creation);
676        }
677    }
678
679    @SuppressWarnings("unchecked")
680    protected <T> void setObjectProperty(NuxeoObjectData object, String key, PropertyData<T> d,
681            List<TypeDefinition> types, boolean creation) {
682        PropertyDefinition<T> pd = null;
683        for (TypeDefinition type : types) {
684            pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
685            if (pd != null) {
686                break;
687            }
688        }
689        if (pd == null) {
690            throw new CmisRuntimeException("Unknown property: " + key);
691        }
692        Object value;
693        if (d == null) {
694            value = null;
695        } else if (pd.getCardinality() == Cardinality.SINGLE) {
696            value = d.getFirstValue();
697        } else {
698            value = d.getValues();
699        }
700        setObjectProperty(object, key, value, pd, creation);
701    }
702
703    protected <T> void setObjectProperty(NuxeoObjectData object, String key, Object value, PropertyDefinition<T> pd,
704            boolean creation) {
705        Updatability updatability = pd.getUpdatability();
706        if (updatability == Updatability.READONLY || (updatability == Updatability.ONCREATE && !creation)) {
707            // log.error("Read-only property, ignored: " + key);
708            return;
709        }
710        if (PropertyIds.OBJECT_TYPE_ID.equals(key) || PropertyIds.LAST_MODIFICATION_DATE.equals(key)) {
711            return;
712        }
713        // TODO avoid constructing property object just to set value
714        NuxeoPropertyDataBase<T> np = (NuxeoPropertyDataBase<T>) NuxeoPropertyData.construct(object, pd, callContext);
715        np.setValue(value);
716    }
717
718    /** Sets initial versioning state and returns its id. */
719    protected String setInitialVersioningState(NuxeoObjectData object, VersioningState versioningState) {
720        if (versioningState == null) {
721            // default is MAJOR, per spec
722            versioningState = VersioningState.MAJOR;
723        }
724        String id;
725        DocumentRef ref = null;
726        switch (versioningState) {
727        case NONE: // cannot be made non-versionable in Nuxeo
728        case CHECKEDOUT:
729            object.doc.setLock();
730            save();
731            id = object.getId();
732            break;
733        case MINOR:
734            ref = object.doc.checkIn(VersioningOption.MINOR, null);
735            save();
736            // id = ref.toString();
737            id = object.getId();
738            break;
739        case MAJOR:
740            ref = object.doc.checkIn(VersioningOption.MAJOR, null);
741            save();
742            // id = ref.toString();
743            id = object.getId();
744            break;
745        default:
746            throw new AssertionError(versioningState);
747        }
748        return id;
749    }
750
751    @Override
752    public String create(String repositoryId, Properties properties, String folderId, ContentStream contentStream,
753            VersioningState versioningState, List<String> policies, ExtensionsData extension) {
754        // TODO policies
755        NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId), null,
756                contentStream);
757        return setInitialVersioningState(object, versioningState);
758    }
759
760    @Override
761    public String createDocument(String repositoryId, Properties properties, String folderId,
762            ContentStream contentStream, VersioningState versioningState, List<String> policies, Acl addAces,
763            Acl removeAces, ExtensionsData extension) {
764        // TODO policies, addAces, removeAces
765        NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId),
766                BaseTypeId.CMIS_DOCUMENT, contentStream);
767        return setInitialVersioningState(object, versioningState);
768    }
769
770    @Override
771    public String createFolder(String repositoryId, Properties properties, String folderId, List<String> policies,
772            Acl addAces, Acl removeAces, ExtensionsData extension) {
773        // TODO policies, addAces, removeAces
774        NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId),
775                BaseTypeId.CMIS_FOLDER, null);
776        return object.getId();
777    }
778
779    @Override
780    public String createPolicy(String repositoryId, Properties properties, String folderId, List<String> policies,
781            Acl addAces, Acl removeAces, ExtensionsData extension) {
782        throw new CmisNotSupportedException();
783    }
784
785    @Override
786    public String createRelationship(String repositoryId, Properties properties, List<String> policies, Acl addAces,
787            Acl removeAces, ExtensionsData extension) {
788        NuxeoObjectData object = createObject(repositoryId, properties, null, BaseTypeId.CMIS_RELATIONSHIP, null);
789        return object.getId();
790    }
791
792    @Override
793    public String createDocumentFromSource(String repositoryId, String sourceId, Properties properties, String folderId,
794            VersioningState versioningState, List<String> policies, Acl addAces, Acl removeAces,
795            ExtensionsData extension) {
796        if (folderId == null) {
797            // no unfileable objects for now
798            throw new CmisInvalidArgumentException("Invalid null folder ID");
799        }
800        DocumentModel doc = getDocumentModel(sourceId);
801        DocumentModel folder = getDocumentModel(folderId);
802        DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
803        NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc);
804        if (properties != null && properties.getPropertyList() != null && !properties.getPropertyList().isEmpty()) {
805            updateProperties(copy, properties, false);
806            copy.doc = coreSession.saveDocument(copyDoc);
807        }
808        save();
809        return setInitialVersioningState(copy, versioningState);
810    }
811
812    public NuxeoObjectData copy(String sourceId, String targetId, Map<String, ?> properties, TypeDefinition type,
813            VersioningState versioningState, List<Policy> policies, List<Ace> addACEs, List<Ace> removeACEs,
814            OperationContext context) {
815        DocumentModel doc = getDocumentModel(sourceId);
816        DocumentModel folder = getDocumentModel(targetId);
817        DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
818        NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc, context);
819        if (properties != null && !properties.isEmpty()) {
820            updateProperties(copy, properties, type, false);
821            copy.doc = coreSession.saveDocument(copyDoc);
822        }
823        save();
824        String id = setInitialVersioningState(copy, versioningState);
825        NuxeoObjectData res;
826        if (id.equals(copy.getId())) {
827            res = copy;
828        } else {
829            // return the version
830            res = new NuxeoObjectData(this, getDocumentModel(id));
831        }
832        return res;
833    }
834
835    @Override
836    public void deleteContentStream(String repositoryId, Holder<String> objectIdHolder,
837            Holder<String> changeTokenHolder, ExtensionsData extension) {
838        setContentStream(repositoryId, objectIdHolder, Boolean.TRUE, changeTokenHolder, null, extension);
839    }
840
841    @Override
842    public FailedToDeleteData deleteTree(String repositoryId, String folderId, Boolean allVersions,
843            UnfileObject unfileObjects, Boolean continueOnFailure, ExtensionsData extension) {
844        if (unfileObjects == UnfileObject.UNFILE) {
845            throw new CmisConstraintException("Unfiling not supported");
846        }
847        if (repository.getRootFolderId().equals(folderId)) {
848            throw new CmisInvalidArgumentException("Cannot delete root");
849        }
850        DocumentModel doc = getDocumentModel(folderId);
851        if (!doc.isFolder()) {
852            throw new CmisInvalidArgumentException("Not a folder: " + folderId);
853        }
854        coreSession.removeDocument(new IdRef(folderId));
855        save();
856        // TODO returning null fails in opencmis 0.1.0 due to
857        // org.apache.chemistry.opencmis.client.runtime.PersistentFolderImpl.deleteTree
858        return new FailedToDeleteDataImpl();
859    }
860
861    @Override
862    public AllowableActions getAllowableActions(String repositoryId, String objectId, ExtensionsData extension) {
863        DocumentModel doc = getDocumentModel(objectId);
864        return NuxeoObjectData.getAllowableActions(doc, false);
865    }
866
867    @Override
868    public ContentStream getContentStream(String repositoryId, String objectId, String streamId, BigInteger offset,
869            BigInteger length, ExtensionsData extension) {
870        // TODO offset, length
871        ContentStream cs;
872        HttpServletRequest request = (HttpServletRequest) callContext.get(CallContext.HTTP_SERVLET_REQUEST);
873        if (streamId == null) {
874            DocumentModel doc = getDocumentModel(objectId);
875            cs = NuxeoPropertyData.getContentStream(doc, request);
876            if (cs == null) {
877                throw new CmisConstraintException("No content stream: " + objectId);
878            }
879        } else {
880            String renditionName = streamId.replaceAll("^" + REND_STREAM_RENDITION_PREFIX, "");
881            cs = getRenditionServiceStream(objectId, renditionName);
882            if (cs == null) {
883                throw new CmisInvalidArgumentException("Invalid stream id: " + streamId);
884            }
885        }
886        if (cs instanceof NuxeoContentStream) {
887            NuxeoContentStream ncs = (NuxeoContentStream) cs;
888            Blob blob = ncs.blob;
889            String blobDigestAlgorithm = blob.getDigestAlgorithm();
890            if (MD5_DIGEST.equals(blobDigestAlgorithm)
891                    && NuxeoContentStream.hasWantDigestRequestHeader(request, CONTENT_MD5_DIGEST_ALGORITHM)) {
892                setResponseHeader(CONTENT_MD5_HEADER_NAME, blob, callContext);
893            }
894            if (NuxeoContentStream.hasWantDigestRequestHeader(request, blobDigestAlgorithm)) {
895                setResponseHeader(DIGEST_HEADER_NAME, blob, callContext);
896            }
897        }
898        return cs;
899    }
900
901    protected void setResponseHeader(String headerName, Blob blob, CallContext callContext) {
902        String digest = NuxeoPropertyData.transcodeHexToBase64(blob.getDigest());
903        HttpServletResponse response = (HttpServletResponse) callContext.get(CallContext.HTTP_SERVLET_RESPONSE);
904        if (DIGEST_HEADER_NAME.equalsIgnoreCase(headerName)) {
905            digest = blob.getDigestAlgorithm() + "=" + digest;
906        }
907        response.setHeader(headerName, digest);
908    }
909
910    /**
911     * @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662.
912     */
913    @Deprecated
914    protected ContentStream getIconRenditionStream(String objectId) {
915        DocumentModel doc = getDocumentModel(objectId);
916        String iconPath;
917        try {
918            iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON);
919        } catch (PropertyException e) {
920            iconPath = null;
921        }
922        InputStream is = NuxeoObjectData.getIconStream(iconPath, callContext);
923        if (is == null) {
924            throw new CmisConstraintException("No icon content stream: " + objectId);
925        }
926
927        int slash = iconPath.lastIndexOf('/');
928        String filename = slash == -1 ? iconPath : iconPath.substring(slash + 1);
929
930        SimpleImageInfo info;
931        try {
932            info = new SimpleImageInfo(is);
933        } catch (IOException e) {
934            throw new CmisRuntimeException(e.toString(), e);
935        }
936        // refetch now-consumed stream
937        is = NuxeoObjectData.getIconStream(iconPath, callContext);
938        return new ContentStreamImpl(filename, BigInteger.valueOf(info.getLength()), info.getMimeType(), is);
939    }
940
941    protected ContentStream getRenditionServiceStream(String objectId, String renditionName) {
942        RenditionService renditionService = Framework.getService(RenditionService.class);
943        DocumentModel doc = getDocumentModel(objectId);
944        Rendition rendition = renditionService.getRendition(doc, renditionName);
945        if (rendition == null) {
946            return null;
947        }
948        Blob blob = rendition.getBlob();
949        if (blob == null) {
950            return null;
951        }
952
953        Calendar modificationDate = rendition.getModificationDate();
954        GregorianCalendar lastModified = (modificationDate instanceof GregorianCalendar)
955                ? (GregorianCalendar) modificationDate : null;
956        HttpServletRequest request = (HttpServletRequest) getCallContext().get(CallContext.HTTP_SERVLET_REQUEST);
957        return NuxeoContentStream.create(doc, null, blob, "cmisRendition",
958                Collections.singletonMap("rendition", renditionName), lastModified, request);
959    }
960
961    @Override
962    public List<RenditionData> getRenditions(String repositoryId, String objectId, String renditionFilter,
963            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
964        if (!NuxeoObjectData.needsRenditions(renditionFilter)) {
965            return Collections.emptyList();
966        }
967        DocumentModel doc = getDocumentModel(objectId);
968        return NuxeoObjectData.getRenditions(doc, renditionFilter, maxItems, skipCount, callContext);
969    }
970
971    @Override
972    public ObjectData getObjectByPath(String repositoryId, String path, String filter, Boolean includeAllowableActions,
973            IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
974            Boolean includeAcl, ExtensionsData extension) {
975        DocumentModel doc;
976        DocumentRef pathRef = new PathRef(path);
977        if (coreSession.exists(pathRef)) {
978            doc = coreSession.getDocument(pathRef);
979            if (isFilteredOut(doc)) {
980                throw new CmisObjectNotFoundException(path);
981            }
982        } else {
983            // Adobe Drive 2 confuses cmis:name and path segment
984            // try using sequence of titles
985            doc = getObjectByPathOfNames(path);
986        }
987        ObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
988                renditionFilter, includePolicyIds, includeAcl, extension);
989        collectObjectInfo(repositoryId, data.getId());
990        return data;
991    }
992
993    /**
994     * Gets a document given a path built out of dc:title components.
995     * <p>
996     * Filtered out docs are ignored.
997     */
998    protected DocumentModel getObjectByPathOfNames(String path) throws CmisObjectNotFoundException {
999        DocumentModel doc = coreSession.getRootDocument();
1000        for (String name : new Path(path).segments()) {
1001            String query = String.format("SELECT * FROM Document WHERE " + NXQL.ECM_PARENTID + " = %s AND "
1002                    + NuxeoTypeHelper.NX_DC_TITLE + " = %s", escapeStringForNXQL(doc.getId()),
1003                    escapeStringForNXQL(name));
1004            query = addProxyClause(query);
1005            DocumentModelList docs = coreSession.query(query);
1006            if (docs.isEmpty()) {
1007                throw new CmisObjectNotFoundException(path);
1008            }
1009            doc = null;
1010            for (DocumentModel d : docs) {
1011                if (isFilteredOut(d)) {
1012                    continue;
1013                }
1014                if (doc == null) {
1015                    doc = d;
1016                } else {
1017                    log.warn(String.format("Path '%s' returns several documents for '%s'", path, name));
1018                    break;
1019                }
1020            }
1021            if (doc == null) {
1022                throw new CmisObjectNotFoundException(path);
1023            }
1024        }
1025        return doc;
1026    }
1027
1028    protected static String REPLACE_QUOTE = Matcher.quoteReplacement("\\'");
1029
1030    protected static String escapeStringForNXQL(String s) {
1031        return "'" + s.replaceAll("'", REPLACE_QUOTE) + "'";
1032    }
1033
1034    @Override
1035    public Properties getProperties(String repositoryId, String objectId, String filter, ExtensionsData extension) {
1036        DocumentModel doc = getDocumentModel(objectId);
1037        NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, null, null, null, null, null, null);
1038        return data.getProperties();
1039    }
1040
1041    protected boolean collectObjectInfos = true;
1042
1043    protected Map<String, ObjectInfo> objectInfos;
1044
1045    // part of CMIS API and of ObjectInfoHandler
1046    @Override
1047    public ObjectInfo getObjectInfo(String repositoryId, String objectId) {
1048        ObjectInfo info = getObjectInfo().get(objectId);
1049        if (info != null) {
1050            return info;
1051        }
1052        DocumentModel doc = getDocumentModel(objectId);
1053        NuxeoObjectData data = new NuxeoObjectData(this, doc, null, Boolean.TRUE, IncludeRelationships.BOTH, "*",
1054                Boolean.TRUE, Boolean.TRUE, null);
1055        return getObjectInfo(repositoryId, data);
1056    }
1057
1058    // AbstractCmisService helper
1059    protected ObjectInfo getObjectInfo(String repositoryId, ObjectData data) {
1060        ObjectInfo info = getObjectInfo().get(data.getId());
1061        if (info != null) {
1062            return info;
1063        }
1064        try {
1065            collectObjectInfos = false;
1066            info = getObjectInfoIntern(repositoryId, data);
1067            getObjectInfo().put(info.getId(), info);
1068        } finally {
1069            collectObjectInfos = true;
1070        }
1071        return info;
1072    }
1073
1074    protected Map<String, ObjectInfo> getObjectInfo() {
1075        if (objectInfos == null) {
1076            objectInfos = new HashMap<>();
1077        }
1078        return objectInfos;
1079    }
1080
1081    @Override
1082    public void clearObjectInfos() {
1083        objectInfos = null;
1084    }
1085
1086    protected void collectObjectInfo(String repositoryId, String objectId) {
1087        if (collectObjectInfos && callContext.isObjectInfoRequired()) {
1088            getObjectInfo(repositoryId, objectId);
1089        }
1090    }
1091
1092    @Override
1093    public void addObjectInfo(ObjectInfo info) {
1094        // ObjectInfoHandler, unused here
1095        throw new UnsupportedOperationException();
1096    }
1097
1098    @Override
1099    public void moveObject(String repositoryId, Holder<String> objectIdHolder, String targetFolderId,
1100            String sourceFolderId, ExtensionsData extension) {
1101        String objectId;
1102        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1103            throw new CmisInvalidArgumentException("Missing object ID");
1104        }
1105        if (repository.getRootFolderId().equals(objectId)) {
1106            throw new CmisConstraintException("Cannot move root");
1107        }
1108        if (targetFolderId == null) {
1109            throw new CmisInvalidArgumentException("Missing target folder ID");
1110        }
1111        getDocumentModel(objectId); // check exists and not deleted
1112        DocumentRef docRef = new IdRef(objectId);
1113        DocumentModel parent = coreSession.getParentDocument(docRef);
1114        if (isFilteredOut(parent)) {
1115            throw new CmisObjectNotFoundException("No parent: " + objectId);
1116        }
1117        if (sourceFolderId != null) {
1118            // check it's the actual parent
1119            if (!parent.getId().equals(sourceFolderId)) {
1120                throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in " + sourceFolderId);
1121            }
1122        }
1123        DocumentModel target = getDocumentModel(targetFolderId);
1124        if (!target.isFolder()) {
1125            throw new CmisInvalidArgumentException("Target is not a folder: " + targetFolderId);
1126        }
1127        coreSession.move(docRef, new IdRef(targetFolderId), null);
1128        save();
1129    }
1130
1131    @Override
1132    public void setContentStream(String repositoryId, Holder<String> objectIdHolder, Boolean overwriteFlag,
1133            Holder<String> changeTokenHolder, ContentStream contentStream, ExtensionsData extension) {
1134        String objectId;
1135        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1136            throw new CmisInvalidArgumentException("Missing object ID");
1137        }
1138        DocumentModel doc = getDocumentModel(objectId);
1139        // TODO test doc checkout state
1140        try {
1141            NuxeoPropertyData.setContentStream(doc, contentStream, !Boolean.FALSE.equals(overwriteFlag));
1142            setChangeTokenForUpdate(doc, changeTokenHolder);
1143            try {
1144                doc = coreSession.saveDocument(doc);
1145            } catch (ConcurrentUpdateException e) {
1146                throw new CmisUpdateConflictException(e.getMessage());
1147            }
1148            NuxeoPropertyData.validateBlobDigest(doc, callContext);
1149            save();
1150        } catch (IOException e) {
1151            throw new CmisRuntimeException(e.toString(), e);
1152        }
1153    }
1154
1155    protected void setChangeTokenForUpdate(DocumentModel doc, Holder<String> changeTokenHolder) {
1156        if (changeTokenHolder == null) {
1157            return;
1158        }
1159        String changeToken = changeTokenHolder.getValue();
1160        if (changeToken == null) {
1161            return;
1162        }
1163        doc.putContextData(CoreSession.CHANGE_TOKEN, changeToken);
1164    }
1165
1166    @Override
1167    public void updateProperties(String repositoryId, Holder<String> objectIdHolder, Holder<String> changeTokenHolder,
1168            Properties properties, ExtensionsData extension) {
1169        updateProperties(objectIdHolder, changeTokenHolder, properties);
1170        save();
1171    }
1172
1173    /* does not save the session */
1174    protected void updateProperties(Holder<String> objectIdHolder, Holder<String> changeTokenHolder,
1175            Properties properties) {
1176        String objectId;
1177        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1178            throw new CmisInvalidArgumentException("Missing object ID");
1179        }
1180        DocumentModel doc = getDocumentModel(objectId);
1181        NuxeoObjectData object = new NuxeoObjectData(this, doc);
1182        updateProperties(object, properties, false);
1183        setChangeTokenForUpdate(doc, changeTokenHolder);
1184        try {
1185            coreSession.saveDocument(doc);
1186        } catch (ConcurrentUpdateException e) {
1187            throw new CmisUpdateConflictException(e.getMessage());
1188        }
1189    }
1190
1191    @Override
1192    public List<BulkUpdateObjectIdAndChangeToken> bulkUpdateProperties(String repositoryId,
1193            List<BulkUpdateObjectIdAndChangeToken> objectIdAndChangeToken, Properties properties,
1194            List<String> addSecondaryTypeIds, List<String> removeSecondaryTypeIds, ExtensionsData extension) {
1195        List<BulkUpdateObjectIdAndChangeToken> list = new ArrayList<>(objectIdAndChangeToken.size());
1196        for (BulkUpdateObjectIdAndChangeToken idt : objectIdAndChangeToken) {
1197            String id = idt.getId();
1198            Holder<String> objectIdHolder = new Holder<>(id);
1199            Holder<String> changeTokenHolder = new Holder<>(idt.getChangeToken());
1200            updateProperties(objectIdHolder, changeTokenHolder, properties);
1201            list.add(new BulkUpdateObjectIdAndChangeTokenImpl(id, objectIdHolder.getValue(),
1202                    changeTokenHolder.getValue()));
1203        }
1204        save();
1205        return list;
1206    }
1207
1208    @Override
1209    public Acl applyAcl(String repositoryId, String objectId, Acl addAces, Acl removeAces,
1210            AclPropagation aclPropagation, ExtensionsData extension) {
1211        return applyAcl(objectId, addAces, removeAces, false, aclPropagation);
1212    }
1213
1214    @Override
1215    public Acl applyAcl(String repositoryId, String objectId, Acl aces, AclPropagation aclPropagation) {
1216        return applyAcl(objectId, aces, null, true, aclPropagation);
1217    }
1218
1219    protected Acl applyAcl(String objectId, Acl addAces, Acl removeAces, boolean clearFirst,
1220            AclPropagation aclPropagation) {
1221        DocumentModel doc = getDocumentModel(objectId); // does filtering
1222        if (aclPropagation == null) {
1223            aclPropagation = AclPropagation.REPOSITORYDETERMINED;
1224        }
1225        if (aclPropagation == AclPropagation.OBJECTONLY && doc.getDocumentType().isFolder()) {
1226            throw new CmisInvalidArgumentException("Cannot use ACLPropagation=objectonly on Folder");
1227        }
1228        DocumentRef docRef = new IdRef(objectId);
1229
1230        ACP acp = coreSession.getACP(docRef);
1231
1232        ACL acl = acp.getOrCreateACL(ACL.LOCAL_ACL);
1233        if (clearFirst) {
1234            acl.clear();
1235        }
1236
1237        if (addAces != null) {
1238            for (Ace ace : addAces.getAces()) {
1239                String principalId = ace.getPrincipalId();
1240                for (String permission : ace.getPermissions()) {
1241                    String perm = permissionToNuxeo(permission);
1242                    if (PERMISSION_NOTHING.equals(perm)) {
1243                        // block everything
1244                        acl.add(new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false));
1245                    } else {
1246                        acl.add(new ACE(principalId, perm, true));
1247                    }
1248                }
1249            }
1250        }
1251
1252        if (removeAces != null) {
1253            for (Iterator<ACE> it = acl.iterator(); it.hasNext();) {
1254                ACE ace = it.next();
1255                String username = ace.getUsername();
1256                String perm = ace.getPermission();
1257                if (ace.isDenied()) {
1258                    if (SecurityConstants.EVERYONE.equals(username) && SecurityConstants.EVERYTHING.equals(perm)) {
1259                        perm = PERMISSION_NOTHING;
1260                    } else {
1261                        continue;
1262                    }
1263                }
1264                String permission = permissionFromNuxeo(perm);
1265                for (Ace race : removeAces.getAces()) {
1266                    String principalId = race.getPrincipalId();
1267                    if (!username.equals(principalId)) {
1268                        continue;
1269                    }
1270                    if (race.getPermissions().contains(permission)) {
1271                        it.remove();
1272                        break;
1273                    }
1274                }
1275            }
1276        }
1277        coreSession.setACP(docRef, acp, true);
1278        return NuxeoObjectData.getAcl(acp, false, this);
1279    }
1280
1281    protected static String permissionToNuxeo(String permission) {
1282        switch (permission) {
1283        case BasicPermissions.READ:
1284            return SecurityConstants.READ;
1285        case BasicPermissions.WRITE:
1286            return SecurityConstants.READ_WRITE;
1287        case BasicPermissions.ALL:
1288            return SecurityConstants.EVERYTHING;
1289        default:
1290            return permission;
1291        }
1292    }
1293
1294    protected static String permissionFromNuxeo(String permission) {
1295        switch (permission) {
1296        case SecurityConstants.READ:
1297            return BasicPermissions.READ;
1298        case SecurityConstants.READ_WRITE:
1299            return BasicPermissions.WRITE;
1300        case SecurityConstants.EVERYTHING:
1301            return BasicPermissions.ALL;
1302        default:
1303            return permission;
1304        }
1305    }
1306
1307    @Override
1308    public Acl getAcl(String repositoryId, String objectId, Boolean onlyBasicPermissions, ExtensionsData extension) {
1309        boolean basic = !Boolean.FALSE.equals(onlyBasicPermissions);
1310        getDocumentModel(objectId); // does filtering
1311        ACP acp = coreSession.getACP(new IdRef(objectId));
1312        return NuxeoObjectData.getAcl(acp, basic, this);
1313    }
1314
1315    @Override
1316    public ObjectList getContentChanges(String repositoryId, Holder<String> changeLogTokenHolder,
1317            Boolean includeProperties, String filter, Boolean includePolicyIds, Boolean includeAcl, BigInteger maxItems,
1318            ExtensionsData extension) {
1319        if (changeLogTokenHolder == null) {
1320            throw new CmisInvalidArgumentException("Missing change log token holder");
1321        }
1322        String changeLogToken = changeLogTokenHolder.getValue();
1323        long minId;
1324        if (changeLogToken == null) {
1325            minId = 0;
1326        } else {
1327            try {
1328                minId = Long.parseLong(changeLogToken);
1329            } catch (NumberFormatException e) {
1330                throw new CmisInvalidArgumentException("Invalid change log token");
1331            }
1332        }
1333        int max = maxItems == null ? -1 : maxItems.intValue();
1334        if (max <= 0) {
1335            max = DEFAULT_CHANGE_LOG_SIZE;
1336        }
1337        if (max > MAX_CHANGE_LOG_SIZE) {
1338            max = MAX_CHANGE_LOG_SIZE;
1339        }
1340        List<ObjectData> ods = null;
1341        // retry with increasingly larger page size if some items are
1342        // skipped
1343        for (int scale = 1; scale < 128; scale *= 2) {
1344            int pageSize = max * scale + 1;
1345            if (pageSize < 0) { // overflow
1346                pageSize = Integer.MAX_VALUE;
1347            }
1348            ods = readAuditLog(repositoryId, minId, max, pageSize);
1349            if (ods != null) {
1350                break;
1351            }
1352            if (pageSize == Integer.MAX_VALUE) {
1353                break;
1354            }
1355        }
1356        if (ods == null) {
1357            // couldn't find enough, too many items were skipped
1358            ods = Collections.emptyList();
1359
1360        }
1361        boolean hasMoreItems = ods.size() > max;
1362        if (hasMoreItems) {
1363            ods = ods.subList(0, max);
1364        }
1365        String latestChangeLogToken;
1366        if (ods.size() == 0) {
1367            latestChangeLogToken = null;
1368        } else {
1369            ObjectData last = ods.get(ods.size() - 1);
1370            latestChangeLogToken = (String) last.getProperties().getProperties().get(NX_CHANGE_LOG_ID).getFirstValue();
1371        }
1372        ObjectListImpl ol = new ObjectListImpl();
1373        ol.setHasMoreItems(Boolean.valueOf(hasMoreItems));
1374        ol.setObjects(ods);
1375        ol.setNumItems(BigInteger.valueOf(-1));
1376        changeLogTokenHolder.setValue(latestChangeLogToken);
1377        return ol;
1378    }
1379
1380    /**
1381     * Reads at most max+1 entries from the audit log.
1382     *
1383     * @return null if not enough elements found with the current page size
1384     */
1385    protected List<ObjectData> readAuditLog(String repositoryId, long minId, int max, int pageSize) {
1386        AuditReader reader = Framework.getService(AuditReader.class);
1387        if (reader == null) {
1388            throw new CmisRuntimeException("Cannot find audit service");
1389        }
1390        List<LogEntry> entries = reader.getLogEntriesAfter(minId, pageSize, repositoryId,
1391                DOCUMENT_CREATED, DOCUMENT_UPDATED, DOCUMENT_REMOVED);
1392        List<ObjectData> ods = new ArrayList<>();
1393        for (LogEntry entry : entries) {
1394            ObjectData od = getLogEntryObjectData(entry);
1395            if (od != null) {
1396                ods.add(od);
1397                if (ods.size() > max) {
1398                    // enough collected
1399                    return ods;
1400                }
1401            }
1402        }
1403        if (entries.size() < pageSize) {
1404            // end of audit log
1405            return ods;
1406        }
1407        return null;
1408    }
1409
1410    /**
1411     * Gets object data for a log entry, or null if skipped.
1412     */
1413    protected ObjectData getLogEntryObjectData(LogEntry logEntry) {
1414        String docType = logEntry.getDocType();
1415        if (!getTypeManager().hasType(docType)) {
1416            // ignore types present in the log but not exposed through CMIS
1417            return null;
1418        }
1419        // change type
1420        String eventId = logEntry.getEventId();
1421        ChangeType changeType;
1422        if (DOCUMENT_CREATED.equals(eventId)) {
1423            changeType = ChangeType.CREATED;
1424        } else if (DOCUMENT_UPDATED.equals(eventId)) {
1425            changeType = ChangeType.UPDATED;
1426        } else if (DOCUMENT_REMOVED.equals(eventId)) {
1427            changeType = ChangeType.DELETED;
1428        } else {
1429            return null;
1430        }
1431        ChangeEventInfoDataImpl cei = new ChangeEventInfoDataImpl();
1432        cei.setChangeType(changeType);
1433        // change time
1434        GregorianCalendar changeTime = (GregorianCalendar) Calendar.getInstance();
1435        Date date = logEntry.getEventDate();
1436        changeTime.setTime(date);
1437        cei.setChangeTime(changeTime);
1438        ObjectDataImpl od = new ObjectDataImpl();
1439        od.setChangeEventInfo(cei);
1440        // properties: id, doc type, change log id
1441        PropertiesImpl properties = new PropertiesImpl();
1442        properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_ID, logEntry.getDocUUID()));
1443        properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_TYPE_ID, docType));
1444        properties.addProperty(new PropertyStringImpl(NX_CHANGE_LOG_ID, String.valueOf(logEntry.getId())));
1445        od.setProperties(properties);
1446        return od;
1447    }
1448
1449    protected String getLatestChangeLogToken(String repositoryId) {
1450        AuditReader reader = Framework.getService(AuditReader.class);
1451        if (reader == null) {
1452            log.warn("Audit Service not found. latest change log token will be '0'");
1453            return "0";
1454            // throw new CmisRuntimeException("Cannot find audit service");
1455        }
1456        long id = reader.getLatestLogId(repositoryId, DOCUMENT_CREATED, DOCUMENT_UPDATED, DOCUMENT_REMOVED);
1457        return String.valueOf(id);
1458    }
1459
1460    protected String addProxyClause(String query) {
1461        if (!repository.supportsProxies()) {
1462            query += " AND " + NXQL.ECM_ISPROXY + " = 0";
1463        }
1464        return query;
1465    }
1466
1467    @Override
1468    public ObjectList query(String repositoryId, String statement, Boolean searchAllVersions,
1469            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1470            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
1471        long skip = skipCount == null ? 0 : skipCount.longValue();
1472        if (skip < 0) {
1473            skip = 0;
1474        }
1475        long max = maxItems == null ? -1 : maxItems.longValue();
1476        if (max <= 0) {
1477            max = DEFAULT_QUERY_SIZE;
1478        }
1479        Map<String, PropertyDefinition<?>> typeInfo = new HashMap<>();
1480        // searchAllVersions defaults to false, spec 2.2.6.1.1
1481        PartialList<Map<String, Serializable>> res = queryProjection(statement, max, skip,
1482                Boolean.TRUE.equals(searchAllVersions), typeInfo);
1483
1484        // convert from Nuxeo to CMIS format
1485        List<ObjectData> list = new ArrayList<>(res.size());
1486        for (Map<String, Serializable> map : res) {
1487            ObjectDataImpl od = makeObjectData(map, typeInfo);
1488
1489            // optional stuff
1490            String id = od.getId();
1491            if (id != null) { // null if JOIN in original query
1492                DocumentModel doc = null;
1493                if (Boolean.TRUE.equals(includeAllowableActions)) {
1494                    doc = getDocumentModel(id);
1495                    AllowableActions allowableActions = NuxeoObjectData.getAllowableActions(doc, false);
1496                    od.setAllowableActions(allowableActions);
1497                }
1498                if (includeRelationships != null && includeRelationships != IncludeRelationships.NONE) {
1499                    // TODO get relationships using a JOIN
1500                    // added to the original query
1501                    List<ObjectData> relationships = NuxeoObjectData.getRelationships(id, includeRelationships, this);
1502                    od.setRelationships(relationships);
1503                }
1504                if (NuxeoObjectData.needsRenditions(renditionFilter)) {
1505                    if (doc == null) {
1506                        doc = getDocumentModel(id);
1507                    }
1508                    List<RenditionData> renditions = NuxeoObjectData.getRenditions(doc, renditionFilter, null, null,
1509                            callContext);
1510                    od.setRenditions(renditions);
1511                }
1512            }
1513
1514            list.add(od);
1515        }
1516        long numItems = res.totalSize();
1517        ObjectListImpl objList = new ObjectListImpl();
1518        objList.setObjects(list);
1519        objList.setNumItems(BigInteger.valueOf(numItems));
1520        objList.setHasMoreItems(Boolean.valueOf(numItems > skip + list.size()));
1521        return objList;
1522    }
1523
1524    /**
1525     * Makes a CMISQL query to the repository and returns an {@link IterableQueryResult}, which MUST be closed in a
1526     * {@code finally} block.
1527     *
1528     * @param query the CMISQL query
1529     * @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
1530     *            ), for versionable types
1531     * @param typeInfo a map filled with type information for each returned property, or {@code null} if no such info is
1532     *            needed
1533     * @return an {@link IterableQueryResult}, which MUST be closed in a {@code finally} block
1534     * @throws CmisInvalidArgumentException if the query cannot be parsed or is invalid
1535     * @since 6.0
1536     */
1537    public IterableQueryResult queryAndFetch(String query, boolean searchAllVersions,
1538            Map<String, PropertyDefinition<?>> typeInfo) {
1539        if (repository.supportsJoins()) {
1540            if (repository.supportsProxies()) {
1541                throw new CmisRuntimeException(
1542                        "Server configuration error: cannot supports joins and proxies at the same time");
1543            }
1544            // straight to CoreSession as CMISQL, relies on proper QueryMaker
1545            return coreSession.queryAndFetch(query, CMISQLQueryMaker.TYPE, this, typeInfo,
1546                    Boolean.valueOf(searchAllVersions));
1547        } else {
1548            // convert to NXQL for evaluation
1549            CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
1550            String nxql;
1551            try {
1552                nxql = converter.getNXQL(query, this, typeInfo, searchAllVersions);
1553            } catch (QueryParseException e) {
1554                throw new CmisInvalidArgumentException(e.getMessage(), e);
1555            }
1556
1557            IterableQueryResult it;
1558            try {
1559                if (repository.useElasticsearch()) {
1560                    ElasticSearchService ess = Framework.getService(ElasticSearchService.class);
1561                    NxQueryBuilder qb = new NxQueryBuilder(coreSession).nxql(nxql)
1562                                                                       .limit(1000)
1563                                                                       .onlyElasticsearchResponse();
1564                    it = new EsIterableQueryResultImpl(ess, ess.scroll(qb, 1000));
1565                } else {
1566                    // distinct documents - new Object[0] is necessary for compilation
1567                    it = coreSession.queryAndFetch(nxql, NXQL.NXQL, true, new Object[0]);
1568                }
1569            } catch (QueryParseException e) {
1570                e.addInfo("Invalid query: CMISQL: " + query);
1571                throw e;
1572            }
1573            // wrap result
1574            return converter.getIterableQueryResult(it, this);
1575        }
1576    }
1577
1578    /**
1579     * Makes a CMISQL query to the repository and returns an {@link IterableQueryResult}, which MUST be closed in a
1580     * {@code finally} block.
1581     *
1582     * @param query the CMISQL query
1583     * @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
1584     *            ), for versionable types
1585     * @return an {@link IterableQueryResult}, which MUST be closed in a {@code finally} block
1586     * @throws CmisRuntimeException if the query cannot be parsed or is invalid
1587     * @since 6.0
1588     */
1589    public IterableQueryResult queryAndFetch(String query, boolean searchAllVersions) {
1590        return queryAndFetch(query, searchAllVersions, null);
1591    }
1592
1593    /**
1594     * Makes a CMISQL query to the repository and returns a {@link PartialList}.
1595     *
1596     * @param query the CMISQL query
1597     * @param limit the maximum number of documents to retrieve, or 0 for all of them
1598     * @param offset the offset (starting at 0) into the list of documents
1599     * @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
1600     *            ), for versionable types
1601     * @param typeInfo a map filled with type information for each returned property, or {@code null} if no such info is
1602     *            needed
1603     * @return a {@link PartialList}
1604     * @throws CmisInvalidArgumentException if the query cannot be parsed or is invalid
1605     * @since 7.10-HF25, 8.10-HF06, 9.2
1606     */
1607    public PartialList<Map<String, Serializable>> queryProjection(String query, long limit, long offset,
1608            boolean searchAllVersions, Map<String, PropertyDefinition<?>> typeInfo) {
1609        if (repository.supportsJoins()) {
1610            if (repository.supportsProxies()) {
1611                throw new CmisRuntimeException(
1612                        "Server configuration error: cannot supports joins and proxies at the same time");
1613            }
1614            // straight to CoreSession as CMISQL, relies on proper QueryMaker
1615            return coreSession.queryProjection(query, CMISQLQueryMaker.TYPE, false, limit, offset, -1, this, typeInfo,
1616                    Boolean.valueOf(searchAllVersions));
1617        } else {
1618            // convert to NXQL for evaluation
1619            CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
1620            String nxql;
1621            try {
1622                nxql = converter.getNXQL(query, this, typeInfo, searchAllVersions);
1623            } catch (QueryParseException e) {
1624                throw new CmisInvalidArgumentException(e.getMessage(), e);
1625            }
1626
1627            PartialList<Map<String, Serializable>> pl;
1628            try {
1629                if (repository.useElasticsearch()) {
1630                    ElasticSearchService ess = Framework.getService(ElasticSearchService.class);
1631                    NxQueryBuilder qb = new NxQueryBuilder(coreSession).nxql(nxql)
1632                                                                       .limit((int) limit)
1633                                                                       .offset((int) offset)
1634                                                                       .onlyElasticsearchResponse();
1635                    SearchResponse esResponse = ess.queryAndAggregate(qb).getElasticsearchResponse();
1636                    // Convert response
1637                    SearchHits esHits = esResponse.getHits();
1638                    List<Map<String, Serializable>> list = new EsSearchHitConverter(
1639                            qb.getSelectFieldsAndTypes()).convert(esHits.getHits());
1640                    pl = new PartialList<>(list, esHits.getTotalHits());
1641                } else {
1642                    // distinct documents
1643                    pl = coreSession.queryProjection(nxql, NXQL.NXQL, true, limit, offset, -1);
1644                }
1645            } catch (QueryParseException e) {
1646                e.addInfo("Invalid query: CMISQL: " + query);
1647                throw e;
1648            }
1649            // wrap result
1650            return converter.convertToCMIS(pl, this);
1651        }
1652    }
1653
1654    protected ObjectDataImpl makeObjectData(Map<String, Serializable> map,
1655            Map<String, PropertyDefinition<?>> typeInfo) {
1656        ObjectDataImpl od = new ObjectDataImpl();
1657        PropertiesImpl properties = new PropertiesImpl();
1658        for (Entry<String, Serializable> en : map.entrySet()) {
1659            String queryName = en.getKey();
1660            PropertyDefinition<?> pd = typeInfo.get(queryName);
1661            if (pd == null) {
1662                throw new NullPointerException("Cannot get " + queryName);
1663            }
1664            AbstractPropertyData<?> p = (AbstractPropertyData<?>) objectFactory.createPropertyData(pd, en.getValue());
1665            p.setLocalName(pd.getLocalName());
1666            p.setDisplayName(pd.getDisplayName());
1667            // queryName and pd.getQueryName() may be different
1668            // for qualified properties
1669            p.setQueryName(queryName);
1670            properties.addProperty(p);
1671        }
1672        od.setProperties(properties);
1673        return od;
1674    }
1675
1676    @Override
1677    public void addObjectToFolder(String repositoryId, String objectId, String folderId, Boolean allVersions,
1678            ExtensionsData extension) {
1679        throw new CmisNotSupportedException();
1680    }
1681
1682    @Override
1683    public void removeObjectFromFolder(String repositoryId, String objectId, String folderId,
1684            ExtensionsData extension) {
1685        if (folderId != null) {
1686            // check it's the actual parent
1687            DocumentModel folder = getDocumentModel(folderId);
1688            DocumentModel parent = coreSession.getParentDocument(new IdRef(objectId));
1689            if (!parent.getId().equals(folder.getId())) {
1690                throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in  " + folderId);
1691            }
1692        }
1693        deleteObject(repositoryId, objectId, Boolean.FALSE, extension);
1694    }
1695
1696    @Override
1697    public ObjectInFolderList getChildren(String repositoryId, String folderId, String filter, String orderBy,
1698            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1699            Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
1700        if (folderId == null) {
1701            throw new CmisInvalidArgumentException("Null folderId");
1702        }
1703        return getChildrenInternal(repositoryId, folderId, filter, orderBy, includeAllowableActions,
1704                includeRelationships, renditionFilter, includePathSegment, maxItems, skipCount, false);
1705    }
1706
1707    protected ObjectInFolderList getChildrenInternal(String repositoryId, String folderId, String filter,
1708            String orderBy, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
1709            String renditionFilter, Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount,
1710            boolean folderOnly) {
1711        ObjectInFolderListImpl result = new ObjectInFolderListImpl();
1712        List<ObjectInFolderData> list = new ArrayList<>();
1713        DocumentModel folder = getDocumentModel(folderId);
1714        if (!folder.isFolder()) {
1715            return null;
1716        }
1717
1718        String query = String.format(
1719                "SELECT * FROM %s WHERE " // Folder/Document
1720                        + "%s = '%s' AND " // ecm:parentId = 'folderId'
1721                        + "%s <> '%s' AND " // ecm:mixinType <> 'HiddenInNavigation'
1722                        + "%s = 0", // ecm:isTrashed = 0
1723                folderOnly ? "Folder" : "Document", //
1724                NXQL.ECM_PARENTID, folderId, //
1725                NXQL.ECM_MIXINTYPE, FacetNames.HIDDEN_IN_NAVIGATION, //
1726                NXQL.ECM_ISTRASHED);
1727        query = addProxyClause(query);
1728        if (!StringUtils.isBlank(orderBy)) {
1729            CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
1730            query += " ORDER BY " + converter.convertOrderBy(orderBy, getTypeManager());
1731        }
1732
1733        long limit = maxItems == null ? 0 : maxItems.longValue();
1734        if (limit < 0) {
1735            limit = 0;
1736        }
1737        long offset = skipCount == null ? 0 : skipCount.longValue();
1738        if (offset < 0) {
1739            offset = 0;
1740        }
1741
1742        DocumentModelList children = coreSession.query(query, null, limit, offset, true);
1743
1744        for (DocumentModel child : children) {
1745            NuxeoObjectData data = new NuxeoObjectData(this, child, filter, includeAllowableActions,
1746                    includeRelationships, renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
1747            ObjectInFolderDataImpl oifd = new ObjectInFolderDataImpl();
1748            oifd.setObject(data);
1749            if (Boolean.TRUE.equals(includePathSegment)) {
1750                oifd.setPathSegment(child.getName());
1751            }
1752            list.add(oifd);
1753            collectObjectInfo(repositoryId, data.getId());
1754        }
1755
1756        Boolean hasMoreItems;
1757        if (limit == 0) {
1758            hasMoreItems = Boolean.FALSE;
1759        } else {
1760            hasMoreItems = Boolean.valueOf(children.totalSize() > offset + limit);
1761        }
1762        result.setObjects(list);
1763        result.setHasMoreItems(hasMoreItems);
1764        result.setNumItems(BigInteger.valueOf(children.totalSize()));
1765        collectObjectInfo(repositoryId, folderId);
1766        return result;
1767    }
1768
1769    @Override
1770    public List<ObjectInFolderContainer> getDescendants(String repositoryId, String folderId, BigInteger depth,
1771            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
1772            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
1773        if (folderId == null) {
1774            throw new CmisInvalidArgumentException("Null folderId");
1775        }
1776        int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
1777        if (levels == 0) {
1778            throw new CmisInvalidArgumentException("Invalid depth: 0");
1779        }
1780        return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
1781                renditionFilter, includePathSegment, 0, levels, false);
1782    }
1783
1784    @Override
1785    public List<ObjectInFolderContainer> getFolderTree(String repositoryId, String folderId, BigInteger depth,
1786            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
1787            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
1788        if (folderId == null) {
1789            throw new CmisInvalidArgumentException("Null folderId");
1790        }
1791        int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
1792        if (levels == 0) {
1793            throw new CmisInvalidArgumentException("Invalid depth: 0");
1794        }
1795        return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
1796                renditionFilter, includePathSegment, 0, levels, true);
1797    }
1798
1799    protected List<ObjectInFolderContainer> getDescendantsInternal(String repositoryId, String folderId, String filter,
1800            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1801            Boolean includePathSegments, int level, int maxLevels, boolean folderOnly) {
1802        if (maxLevels != -1 && level >= maxLevels) {
1803            return null;
1804        }
1805        ObjectInFolderList children = getChildrenInternal(repositoryId, folderId, filter, null, includeAllowableActions,
1806                includeRelationships, renditionFilter, includePathSegments, null, null, folderOnly);
1807        if (children == null) {
1808            return Collections.emptyList();
1809        }
1810        List<ObjectInFolderContainer> res = new ArrayList<>(children.getObjects().size());
1811        for (ObjectInFolderData child : children.getObjects()) {
1812            ObjectInFolderContainerImpl oifc = new ObjectInFolderContainerImpl();
1813            oifc.setObject(child);
1814            // recurse
1815            List<ObjectInFolderContainer> subChildren = getDescendantsInternal(repositoryId, child.getObject().getId(),
1816                    filter, includeAllowableActions, includeRelationships, renditionFilter, includePathSegments,
1817                    level + 1, maxLevels, folderOnly);
1818            if (subChildren != null) {
1819                oifc.setChildren(subChildren);
1820            }
1821            res.add(oifc);
1822        }
1823        return res;
1824    }
1825
1826    @Override
1827    public ObjectData getFolderParent(String repositoryId, String folderId, String filter, ExtensionsData extension) {
1828        List<ObjectParentData> parents = getObjectParentsInternal(repositoryId, folderId, filter, null, null, null,
1829                Boolean.TRUE, true);
1830        return parents.isEmpty() ? null : parents.get(0).getObject();
1831    }
1832
1833    @Override
1834    public List<ObjectParentData> getObjectParents(String repositoryId, String objectId, String filter,
1835            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1836            Boolean includeRelativePathSegment, ExtensionsData extension) {
1837        return getObjectParentsInternal(repositoryId, objectId, filter, includeAllowableActions, includeRelationships,
1838                renditionFilter, includeRelativePathSegment, false);
1839    }
1840
1841    protected List<ObjectParentData> getObjectParentsInternal(String repositoryId, String objectId, String filter,
1842            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1843            Boolean includeRelativePathSegment, boolean folderOnly) {
1844        DocumentRef docRef = new IdRef(objectId);
1845        if (!coreSession.exists(docRef)) {
1846            throw new CmisObjectNotFoundException(objectId);
1847        }
1848        DocumentModel doc = coreSession.getDocument(docRef);
1849        if (isFilteredOut(doc)) {
1850            throw new CmisObjectNotFoundException(objectId);
1851        }
1852        if (folderOnly && !doc.isFolder()) {
1853            throw new CmisInvalidArgumentException("Not a folder: " + objectId);
1854        }
1855        String pathSegment = doc.getName();
1856        if (pathSegment == null) { // root
1857            return Collections.emptyList();
1858        }
1859        DocumentRef parentRef = doc.getParentRef();
1860        if (parentRef == null) { // placeless
1861            return Collections.emptyList();
1862        }
1863        if (!coreSession.exists(parentRef)) { // non-accessible
1864            return Collections.emptyList();
1865        }
1866        DocumentModel parent = coreSession.getDocument(parentRef);
1867        if (isFilteredOut(parent)) { // filtered out
1868            return Collections.emptyList();
1869        }
1870        String parentId = parent.getId();
1871
1872        ObjectData od = getObject(repositoryId, parentId, filter, includeAllowableActions, includeRelationships,
1873                renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
1874        ObjectParentDataImpl opd = new ObjectParentDataImpl(od);
1875        if (!Boolean.FALSE.equals(includeRelativePathSegment)) {
1876            opd.setRelativePathSegment(pathSegment);
1877        }
1878        return Collections.<ObjectParentData> singletonList(opd);
1879    }
1880
1881    @Override
1882    public void applyPolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
1883        throw new CmisNotSupportedException();
1884    }
1885
1886    @Override
1887    public List<ObjectData> getAppliedPolicies(String repositoryId, String objectId, String filter,
1888            ExtensionsData extension) {
1889        return Collections.emptyList();
1890    }
1891
1892    @Override
1893    public void removePolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
1894        throw new CmisNotSupportedException();
1895    }
1896
1897    @Override
1898    public ObjectList getObjectRelationships(String repositoryId, String objectId, Boolean includeSubRelationshipTypes,
1899            RelationshipDirection relationshipDirection, String typeId, String filter, Boolean includeAllowableActions,
1900            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
1901        IncludeRelationships includeRelationships;
1902        if (relationshipDirection == null || relationshipDirection == RelationshipDirection.SOURCE) {
1903            includeRelationships = IncludeRelationships.SOURCE;
1904        } else if (relationshipDirection == RelationshipDirection.TARGET) {
1905            includeRelationships = IncludeRelationships.TARGET;
1906        } else { // RelationshipDirection.EITHER
1907            includeRelationships = IncludeRelationships.BOTH;
1908        }
1909        List<ObjectData> rels = NuxeoObjectData.getRelationships(objectId, includeRelationships, this);
1910        BatchedList<ObjectData> batch = ListUtils.getBatchedList(rels, maxItems, skipCount, DEFAULT_MAX_RELATIONSHIPS);
1911        ObjectListImpl res = new ObjectListImpl();
1912        res.setObjects(batch.getList());
1913        res.setNumItems(batch.getNumItems());
1914        res.setHasMoreItems(batch.getHasMoreItems());
1915        for (ObjectData data : res.getObjects()) {
1916            collectObjectInfo(repositoryId, data.getId());
1917        }
1918        return res;
1919    }
1920
1921    @Override
1922    public void checkIn(String repositoryId, Holder<String> objectIdHolder, Boolean major, Properties properties,
1923            ContentStream contentStream, String checkinComment, List<String> policies, Acl addAces, Acl removeAces,
1924            ExtensionsData extension) {
1925        String objectId;
1926        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1927            throw new CmisInvalidArgumentException("Missing object ID");
1928        }
1929        VersioningOption option = Boolean.TRUE.equals(major) ? VersioningOption.MAJOR : VersioningOption.MINOR;
1930
1931        DocumentModel doc = getDocumentModel(objectId);
1932
1933        NuxeoObjectData object = new NuxeoObjectData(this, doc);
1934        updateProperties(object, properties, false);
1935        boolean setContentStream = contentStream != null;
1936        if (setContentStream) {
1937            try {
1938                NuxeoPropertyData.setContentStream(doc, contentStream, true);
1939            } catch (IOException e) {
1940                throw new CmisRuntimeException(e.toString(), e);
1941            }
1942        }
1943        // comment for save event
1944        doc.putContextData("comment", checkinComment);
1945        doc = coreSession.saveDocument(doc);
1946        if (setContentStream) {
1947            NuxeoPropertyData.validateBlobDigest(doc, callContext);
1948        }
1949        DocumentRef ver;
1950        try {
1951            ver = doc.checkIn(option, checkinComment);
1952        } catch (VersionNotModifiableException e) {
1953            throw new CmisInvalidArgumentException("Cannot check in non-PWC: " + doc);
1954        }
1955        doc.removeLock();
1956        save();
1957        objectIdHolder.setValue(getIdFromDocumentRef(ver));
1958    }
1959
1960    @Override
1961    public void checkOut(String repositoryId, Holder<String> objectIdHolder, ExtensionsData extension,
1962            Holder<Boolean> contentCopiedHolder) {
1963        String objectId;
1964        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1965            throw new CmisInvalidArgumentException("Missing object ID");
1966        }
1967        String pwcId = checkOut(objectId);
1968        objectIdHolder.setValue(pwcId);
1969        if (contentCopiedHolder != null) {
1970            contentCopiedHolder.setValue(Boolean.TRUE);
1971        }
1972    }
1973
1974    public String checkOut(String objectId) {
1975        DocumentModel doc = getDocumentModel(objectId);
1976        try {
1977            // find pwc
1978            DocumentModel pwc;
1979            if (doc.isVersion()) {
1980                pwc = coreSession.getWorkingCopy(doc.getRef());
1981                if (pwc == null) {
1982                    // no live document available
1983                    // TODO do a restore somewhere
1984                    throw new CmisObjectNotFoundException(objectId);
1985                }
1986            } else {
1987                pwc = doc;
1988            }
1989            if (pwc.isCheckedOut()) {
1990                throw new CmisConstraintException("Already checked out: " + objectId);
1991            }
1992            if (pwc.isLocked()) {
1993                throw new CmisConstraintException("Cannot check out since currently locked: " + objectId);
1994            }
1995            pwc.setLock();
1996            pwc.checkOut();
1997            save();
1998            return pwc.getId();
1999        } catch (VersionNotModifiableException e) {
2000            throw new CmisInvalidArgumentException("Cannot check out non-version: " + objectId);
2001        } catch (NuxeoException e) { // TODO use a core LockException
2002            String message = e.getMessage();
2003            if (message != null && message.startsWith("Document already locked")) {
2004                throw new CmisConstraintException("Cannot check out since currently locked: " + objectId);
2005            }
2006            throw new CmisRuntimeException(e.toString(), e);
2007        }
2008    }
2009
2010    @Override
2011    public void cancelCheckOut(String repositoryId, String objectId, ExtensionsData extension) {
2012        cancelCheckOut(objectId);
2013    }
2014
2015    public void cancelCheckOut(String objectId) {
2016        DocumentModel doc = getDocumentModel(objectId);
2017        if (!doc.isCheckedOut()) {
2018            throw new CmisInvalidArgumentException("Cannot cancel check out of non-PWC: " + doc);
2019        }
2020        DocumentRef docRef = doc.getRef();
2021        // find last version
2022        DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
2023        if (verRef == null) {
2024            if (errorOnCancelCheckOutOfDraftVersion && "0.0".equals(doc.getVersionLabel())) {
2025                throw new CmisVersioningException("Cannot cancelCheckOut of draft version due to configuration");
2026            }
2027            // delete
2028            coreSession.removeDocument(docRef);
2029        } else {
2030            // restore and keep checked in
2031            coreSession.restoreToVersion(docRef, verRef, true, true);
2032            doc.removeLock();
2033        }
2034        save();
2035    }
2036
2037    @Override
2038    public ObjectList getCheckedOutDocs(String repositoryId, String folderId, String filter, String orderBy,
2039            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
2040            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
2041        // columns from filter
2042        List<String> props;
2043        if (StringUtils.isBlank(filter)) {
2044            props = Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID, PropertyIds.BASE_TYPE_ID);
2045        } else {
2046            props = NuxeoObjectData.getPropertyIdsFromFilter(filter);
2047            // same as query names
2048        }
2049        // clause from folderId
2050        List<String> clauses = new ArrayList<>(3);
2051        clauses.add(NuxeoTypeHelper.NX_ISVERSION + " = false");
2052        clauses.add(NuxeoTypeHelper.NX_ISCHECKEDIN + " = false");
2053        if (folderId != null) {
2054            String qid = "'" + folderId.replace("'", "''") + "'";
2055            clauses.add("IN_FOLDER(" + qid + ")");
2056        }
2057        // orderBy
2058        String order;
2059        if (StringUtils.isBlank(orderBy)) {
2060            order = "";
2061        } else {
2062            order = " ORDER BY " + orderBy;
2063        }
2064        String statement = "SELECT " + StringUtils.join(props, ", ") + " FROM " + BaseTypeId.CMIS_DOCUMENT.value()
2065                + " WHERE " + StringUtils.join(clauses, " AND ") + order;
2066        Boolean searchAllVersions = Boolean.TRUE;
2067        return query(repositoryId, statement, searchAllVersions, includeAllowableActions, includeRelationships,
2068                renditionFilter, maxItems, skipCount, extension);
2069    }
2070
2071    @Override
2072    public List<ObjectData> getAllVersions(String repositoryId, String objectId, String versionSeriesId, String filter,
2073            Boolean includeAllowableActions, ExtensionsData extension) {
2074        DocumentModel doc;
2075        if (objectId != null) {
2076            // atompub passes object id
2077            doc = getDocumentModel(objectId);
2078        } else if (versionSeriesId != null) {
2079            // soap passes version series id
2080            // version series id is (for now) id of live document
2081            // TODO deal with removal of live doc
2082            doc = getDocumentModel(versionSeriesId);
2083        } else {
2084            throw new CmisInvalidArgumentException("Missing object ID or version series ID");
2085        }
2086        List<DocumentRef> versions = coreSession.getVersionsRefs(doc.getRef());
2087        List<ObjectData> list = new ArrayList<>(versions.size());
2088        for (DocumentRef verRef : versions) {
2089            // First check if we have enough permission on versions
2090            if (coreSession.hasPermission(verRef, SecurityConstants.READ)) {
2091                String verId = getIdFromDocumentRef(verRef);
2092                ObjectData od = getObject(repositoryId, verId, filter, includeAllowableActions,
2093                        IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, null);
2094                list.add(od);
2095            }
2096        }
2097        // PWC last
2098        DocumentModel pwc = doc.isVersion() ? coreSession.getWorkingCopy(doc.getRef()) : doc;
2099        if (pwc != null && pwc.isCheckedOut()) {
2100            NuxeoObjectData od = new NuxeoObjectData(this, pwc, filter, includeAllowableActions,
2101                    IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, extension);
2102            list.add(od);
2103        }
2104        // CoreSession returns them in creation order,
2105        // CMIS wants them last first
2106        Collections.reverse(list);
2107        return list;
2108    }
2109
2110    @Override
2111    public NuxeoObjectData getObjectOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
2112            Boolean major, String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
2113            String renditionFilter, Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
2114        DocumentModel doc;
2115        if (objectId != null) {
2116            // atompub passes object id
2117            doc = getDocumentModel(objectId);
2118        } else if (versionSeriesId != null) {
2119            // soap passes version series id
2120            // version series id is (for now) id of live document
2121            // TODO deal with removal of live doc
2122            doc = getDocumentModel(versionSeriesId);
2123        } else {
2124            throw new CmisInvalidArgumentException("Missing object ID or version series ID");
2125        }
2126        if (Boolean.TRUE.equals(major)) {
2127            // we must list all versions
2128            List<DocumentModel> versions = coreSession.getVersions(doc.getRef());
2129            Collections.reverse(versions);
2130            for (DocumentModel ver : versions) {
2131                if (ver.isMajorVersion()) {
2132                    return getObject(repositoryId, ver.getId(), filter, includeAllowableActions, includeRelationships,
2133                            renditionFilter, includePolicyIds, includeAcl, null);
2134                }
2135            }
2136            return null;
2137        } else {
2138            DocumentRef verRef = coreSession.getLastDocumentVersionRef(doc.getRef());
2139            String verId = getIdFromDocumentRef(verRef);
2140            return getObject(repositoryId, verId, filter, includeAllowableActions, includeRelationships,
2141                    renditionFilter, includePolicyIds, includeAcl, null);
2142        }
2143    }
2144
2145    @Override
2146    public Properties getPropertiesOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
2147            Boolean major, String filter, ExtensionsData extension) {
2148        NuxeoObjectData od = getObjectOfLatestVersion(repositoryId, objectId, versionSeriesId, major, filter,
2149                Boolean.FALSE, IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, null);
2150        return od == null ? null : od.getProperties();
2151    }
2152
2153    @Override
2154    public void deleteObject(String repositoryId, String objectId, Boolean allVersions, ExtensionsData extension) {
2155        DocumentModel doc = getDocumentModel(objectId);
2156        if (doc.isFolder()) {
2157            // check that there are no children left
2158            DocumentModelList docs = coreSession.getChildren(new IdRef(objectId), null, documentFilter, null);
2159            if (docs.size() > 0) {
2160                throw new CmisConstraintException("Cannot delete non-empty folder: " + objectId);
2161            }
2162        }
2163        coreSession.removeDocument(doc.getRef());
2164        save();
2165    }
2166
2167    @Override
2168    public void deleteObjectOrCancelCheckOut(String repositoryId, String objectId, Boolean allVersions,
2169            ExtensionsData extension) {
2170        DocumentModel doc = getDocumentModel(objectId);
2171        DocumentRef docRef = doc.getRef();
2172        // find last version
2173        DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
2174        // If doc has versions, is locked, and is checkedOut, then it was
2175        // likely
2176        // explicitly checkedOut so invoke cancelCheckOut not delete
2177        if (verRef != null && doc.isLocked() && doc.isCheckedOut()) {
2178            cancelCheckOut(repositoryId, objectId, extension);
2179        } else {
2180            deleteObject(repositoryId, objectId, allVersions, extension);
2181        }
2182    }
2183
2184}