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