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