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