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