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