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