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}