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