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