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