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