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