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.apache.chemistry.opencmis.commons.impl.Constants.RENDITION_NONE; 015 016import java.io.IOException; 017import java.io.InputStream; 018import java.io.Serializable; 019import java.math.BigInteger; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collections; 023import java.util.EnumSet; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.LinkedHashMap; 027import java.util.LinkedHashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Set; 032 033import javax.servlet.ServletContext; 034 035import org.apache.chemistry.opencmis.client.api.OperationContext; 036import org.apache.chemistry.opencmis.commons.BasicPermissions; 037import org.apache.chemistry.opencmis.commons.PropertyIds; 038import org.apache.chemistry.opencmis.commons.data.Ace; 039import org.apache.chemistry.opencmis.commons.data.Acl; 040import org.apache.chemistry.opencmis.commons.data.AllowableActions; 041import org.apache.chemistry.opencmis.commons.data.ChangeEventInfo; 042import org.apache.chemistry.opencmis.commons.data.CmisExtensionElement; 043import org.apache.chemistry.opencmis.commons.data.ExtensionsData; 044import org.apache.chemistry.opencmis.commons.data.MutableAce; 045import org.apache.chemistry.opencmis.commons.data.MutableAcl; 046import org.apache.chemistry.opencmis.commons.data.ObjectData; 047import org.apache.chemistry.opencmis.commons.data.PolicyIdList; 048import org.apache.chemistry.opencmis.commons.data.Properties; 049import org.apache.chemistry.opencmis.commons.data.PropertyData; 050import org.apache.chemistry.opencmis.commons.data.RenditionData; 051import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition; 052import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition; 053import org.apache.chemistry.opencmis.commons.enums.Action; 054import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; 055import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships; 056import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; 057import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlEntryImpl; 058import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl; 059import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlPrincipalDataImpl; 060import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl; 061import org.apache.chemistry.opencmis.commons.impl.dataobjects.BindingsObjectFactoryImpl; 062import org.apache.chemistry.opencmis.commons.impl.dataobjects.PolicyIdListImpl; 063import org.apache.chemistry.opencmis.commons.impl.dataobjects.RenditionDataImpl; 064import org.apache.chemistry.opencmis.commons.server.CallContext; 065import org.apache.chemistry.opencmis.commons.server.CmisService; 066import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory; 067import org.apache.commons.lang.StringUtils; 068import org.nuxeo.ecm.core.api.Blob; 069import org.nuxeo.ecm.core.api.DocumentModel; 070import org.nuxeo.ecm.core.api.IterableQueryResult; 071import org.nuxeo.ecm.core.api.PropertyException; 072import org.nuxeo.ecm.core.api.security.ACE; 073import org.nuxeo.ecm.core.api.security.ACL; 074import org.nuxeo.ecm.core.api.security.ACP; 075import org.nuxeo.ecm.core.api.security.SecurityConstants; 076import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 077import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils; 078import org.nuxeo.ecm.core.opencmis.impl.util.SimpleImageInfo; 079import org.nuxeo.ecm.platform.rendition.Rendition; 080import org.nuxeo.ecm.platform.rendition.service.RenditionDefinition; 081import org.nuxeo.ecm.platform.rendition.service.RenditionService; 082import org.nuxeo.runtime.api.Framework; 083 084/** 085 * Nuxeo implementation of a CMIS {@link ObjectData}, backed by a {@link DocumentModel}. 086 */ 087public class NuxeoObjectData implements ObjectData { 088 089 public static final String REND_STREAM_ICON = "nuxeo:icon"; 090 091 public static final String REND_KIND_CMIS_THUMBNAIL = "cmis:thumbnail"; 092 093 public static final String REND_STREAM_RENDITION_PREFIX = "nuxeo:rendition:"; 094 095 public static final String REND_KIND_NUXEO_RENDITION = "nuxeo:rendition"; 096 097 /** 098 * Property to determine whether all renditions provide a computed size and length. 099 * 100 * @since 7.4 101 */ 102 public static final String RENDITION_COMPUTE_INFO_PROP = "org.nuxeo.cmis.computeRenditionInfo"; 103 104 /** 105 * Default for {@value #RENDITION_COMPUTE_INFO_PROP}. 106 * 107 * @since 7.4 108 */ 109 public static final String RENDITION_COMPUTE_INFO_DEFAULT = "false"; 110 111 public CmisService service; 112 113 public DocumentModel doc; 114 115 public boolean creation = false; // TODO 116 117 private List<String> propertyIds; 118 119 private Boolean includeAllowableActions; 120 121 private IncludeRelationships includeRelationships; 122 123 private String renditionFilter; 124 125 private Boolean includePolicyIds; 126 127 private Boolean includeAcl; 128 129 private static final BindingsObjectFactory objectFactory = new BindingsObjectFactoryImpl(); 130 131 private TypeDefinition type; 132 133 private static final int CACHE_MAX_SIZE = 10; 134 135 private static final int DEFAULT_MAX_RENDITIONS = 20; 136 137 /** Cache for Properties objects, which are expensive to create. */ 138 private Map<String, Properties> propertiesCache = new HashMap<String, Properties>(); 139 140 private CallContext callContext; 141 142 private NuxeoCmisService nuxeoCmisService; 143 144 public NuxeoObjectData(CmisService service, DocumentModel doc, String filter, Boolean includeAllowableActions, 145 IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds, 146 Boolean includeAcl, ExtensionsData extension) { 147 this.service = service; 148 this.doc = doc; 149 propertyIds = getPropertyIdsFromFilter(filter); 150 this.includeAllowableActions = includeAllowableActions; 151 this.includeRelationships = includeRelationships; 152 this.renditionFilter = renditionFilter; 153 this.includePolicyIds = includePolicyIds; 154 this.includeAcl = includeAcl; 155 nuxeoCmisService = NuxeoCmisService.extractFromCmisService(service); 156 type = nuxeoCmisService.repository.getTypeDefinition(NuxeoTypeHelper.mappedId(doc.getType())); 157 callContext = nuxeoCmisService.callContext; 158 } 159 160 protected NuxeoObjectData(CmisService service, DocumentModel doc) { 161 this(service, doc, null, null, null, null, null, null, null); 162 } 163 164 public NuxeoObjectData(CmisService service, DocumentModel doc, OperationContext context) { 165 this(service, doc, context.getFilterString(), Boolean.valueOf(context.isIncludeAllowableActions()), 166 context.getIncludeRelationships(), context.getRenditionFilterString(), 167 Boolean.valueOf(context.isIncludePolicies()), Boolean.valueOf(context.isIncludeAcls()), null); 168 } 169 170 private static final String STAR = "*"; 171 172 protected static final List<String> STAR_FILTER = Collections.singletonList(STAR); 173 174 protected static List<String> getPropertyIdsFromFilter(String filter) { 175 if (filter == null || filter.length() == 0) 176 return STAR_FILTER; 177 else { 178 List<String> ids = Arrays.asList(filter.split(",\\s*")); 179 if (ids.contains(STAR)) { 180 ids = STAR_FILTER; 181 } 182 return ids; 183 } 184 } 185 186 @Override 187 public String getId() { 188 return doc.getId(); 189 } 190 191 @Override 192 public BaseTypeId getBaseTypeId() { 193 return NuxeoTypeHelper.getBaseTypeId(doc); 194 } 195 196 public TypeDefinition getTypeDefinition() { 197 return type; 198 } 199 200 @Override 201 public Properties getProperties() { 202 return getProperties(propertyIds); 203 } 204 205 protected Properties getProperties(List<String> propertyIds) { 206 // for STAR_FILTER the key is equal to STAR (see limitCacheSize) 207 String key = StringUtils.join(propertyIds, ','); 208 Properties properties = propertiesCache.get(key); 209 if (properties == null) { 210 Map<String, PropertyDefinition<?>> propertyDefinitions = type.getPropertyDefinitions(); 211 int len = propertyIds == STAR_FILTER ? propertyDefinitions.size() : propertyIds.size(); 212 List<PropertyData<?>> props = new ArrayList<PropertyData<?>>(len); 213 for (PropertyDefinition<?> pd : propertyDefinitions.values()) { 214 if (propertyIds == STAR_FILTER || propertyIds.contains(pd.getId())) { 215 props.add((PropertyData<?>) NuxeoPropertyData.construct(this, pd, callContext)); 216 } 217 } 218 properties = objectFactory.createPropertiesData(props); 219 limitCacheSize(); 220 propertiesCache.put(key, properties); 221 } 222 return properties; 223 } 224 225 /** Limits cache size, always keeps STAR filter. */ 226 protected void limitCacheSize() { 227 if (propertiesCache.size() >= CACHE_MAX_SIZE) { 228 Properties sf = propertiesCache.get(STAR); 229 propertiesCache.clear(); 230 if (sf != null) { 231 propertiesCache.put(STAR, sf); 232 } 233 } 234 } 235 236 public NuxeoPropertyDataBase<?> getProperty(String id) { 237 // make use of cache 238 return (NuxeoPropertyDataBase<?>) getProperties(STAR_FILTER).getProperties().get(id); 239 } 240 241 @Override 242 public AllowableActions getAllowableActions() { 243 if (!Boolean.TRUE.equals(includeAllowableActions)) { 244 return null; 245 } 246 return getAllowableActions(doc, creation); 247 } 248 249 public static AllowableActions getAllowableActions(DocumentModel doc, boolean creation) { 250 BaseTypeId baseType = NuxeoTypeHelper.getBaseTypeId(doc); 251 boolean isDocument = baseType == BaseTypeId.CMIS_DOCUMENT; 252 boolean isFolder = baseType == BaseTypeId.CMIS_FOLDER; 253 boolean isRoot = "/".equals(doc.getPathAsString()); 254 boolean canWrite = creation || doc.getCoreSession().hasPermission(doc.getRef(), SecurityConstants.WRITE); 255 256 Set<Action> set = EnumSet.noneOf(Action.class); 257 set.add(Action.CAN_GET_OBJECT_PARENTS); 258 set.add(Action.CAN_GET_PROPERTIES); 259 if (isFolder) { 260 set.add(Action.CAN_GET_DESCENDANTS); 261 set.add(Action.CAN_GET_FOLDER_TREE); 262 set.add(Action.CAN_GET_CHILDREN); 263 if (!isRoot) { 264 set.add(Action.CAN_GET_FOLDER_PARENT); 265 } 266 } else if (isDocument) { 267 set.add(Action.CAN_GET_CONTENT_STREAM); 268 set.add(Action.CAN_GET_ALL_VERSIONS); 269 set.add(Action.CAN_ADD_OBJECT_TO_FOLDER); 270 set.add(Action.CAN_REMOVE_OBJECT_FROM_FOLDER); 271 if (doc.isCheckedOut()) { 272 set.add(Action.CAN_CHECK_IN); 273 set.add(Action.CAN_CANCEL_CHECK_OUT); 274 } else { 275 set.add(Action.CAN_CHECK_OUT); 276 } 277 } 278 if (isFolder || isDocument) { 279 set.add(Action.CAN_GET_RENDITIONS); 280 } 281 if (canWrite) { 282 if (isFolder) { 283 set.add(Action.CAN_CREATE_DOCUMENT); 284 set.add(Action.CAN_CREATE_FOLDER); 285 set.add(Action.CAN_CREATE_RELATIONSHIP); 286 set.add(Action.CAN_DELETE_TREE); 287 } else if (isDocument) { 288 set.add(Action.CAN_SET_CONTENT_STREAM); 289 set.add(Action.CAN_DELETE_CONTENT_STREAM); 290 } 291 set.add(Action.CAN_UPDATE_PROPERTIES); 292 if (isFolder && !isRoot || isDocument) { 293 // Relationships are not fileable 294 set.add(Action.CAN_MOVE_OBJECT); 295 } 296 if (!isRoot) { 297 set.add(Action.CAN_DELETE_OBJECT); 298 } 299 } 300 if (Boolean.FALSE.booleanValue()) { 301 // TODO 302 set.add(Action.CAN_GET_OBJECT_RELATIONSHIPS); 303 set.add(Action.CAN_APPLY_POLICY); 304 set.add(Action.CAN_REMOVE_POLICY); 305 set.add(Action.CAN_GET_APPLIED_POLICIES); 306 set.add(Action.CAN_GET_ACL); 307 set.add(Action.CAN_APPLY_ACL); 308 set.add(Action.CAN_CREATE_ITEM); 309 } 310 311 AllowableActionsImpl aa = new AllowableActionsImpl(); 312 aa.setAllowableActions(set); 313 return aa; 314 } 315 316 @Override 317 public List<RenditionData> getRenditions() { 318 if (!needsRenditions(renditionFilter)) { 319 return Collections.emptyList(); 320 } 321 return getRenditions(doc, renditionFilter, null, null, callContext); 322 } 323 324 public static boolean needsRenditions(String renditionFilter) { 325 return !StringUtils.isBlank(renditionFilter) && !RENDITION_NONE.equals(renditionFilter); 326 } 327 328 public static List<RenditionData> getRenditions(DocumentModel doc, String renditionFilter, BigInteger maxItems, 329 BigInteger skipCount, CallContext callContext) { 330 try { 331 List<RenditionData> list = new ArrayList<RenditionData>(); 332 list.addAll(getRenditionServiceRenditions(doc, callContext)); 333 // rendition filter 334 if (!STAR.equals(renditionFilter)) { 335 String[] filters = renditionFilter.split(","); 336 for (Iterator<RenditionData> it = list.iterator(); it.hasNext();) { 337 RenditionData ren = it.next(); 338 boolean keep = false; 339 for (String filter : filters) { 340 if (filter.contains("/")) { 341 // mimetype 342 if (filter.endsWith("/*")) { 343 String typeSlash = filter.substring(0, filter.indexOf('/') + 1); 344 if (ren.getMimeType().startsWith(typeSlash)) { 345 keep = true; 346 break; 347 } 348 } else { 349 if (ren.getMimeType().equals(filter)) { 350 keep = true; 351 break; 352 } 353 } 354 } else { 355 // kind 356 if (ren.getKind().equals(filter)) { 357 keep = true; 358 break; 359 } 360 } 361 } 362 if (!keep) { 363 it.remove(); 364 } 365 } 366 } 367 list = ListUtils.batchList(list, maxItems, skipCount, DEFAULT_MAX_RENDITIONS); 368 return list; 369 } catch (IOException e) { 370 throw new CmisRuntimeException(e.toString(), e); 371 } 372 } 373 374 /** 375 * @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662. 376 */ 377 @Deprecated 378 protected static List<RenditionData> getIconRendition(DocumentModel doc, CallContext callContext) 379 throws IOException { 380 String iconPath; 381 try { 382 iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON); 383 } catch (PropertyException e) { 384 iconPath = null; 385 } 386 InputStream is = getIconStream(iconPath, callContext); 387 if (is == null) { 388 return Collections.emptyList(); 389 } 390 RenditionDataImpl ren = new RenditionDataImpl(); 391 ren.setStreamId(REND_STREAM_ICON); 392 ren.setKind(REND_KIND_CMIS_THUMBNAIL); 393 int slash = iconPath.lastIndexOf('/'); 394 String filename = slash == -1 ? iconPath : iconPath.substring(slash + 1); 395 ren.setTitle(filename); 396 SimpleImageInfo info = new SimpleImageInfo(is); 397 ren.setBigLength(BigInteger.valueOf(info.getLength())); 398 ren.setBigWidth(BigInteger.valueOf(info.getWidth())); 399 ren.setBigHeight(BigInteger.valueOf(info.getHeight())); 400 ren.setMimeType(info.getMimeType()); 401 return Collections.<RenditionData> singletonList(ren); 402 } 403 404 /** 405 * @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662. 406 */ 407 @Deprecated 408 public static InputStream getIconStream(String iconPath, CallContext context) { 409 if (iconPath == null || iconPath.length() == 0) { 410 return null; 411 } 412 if (!iconPath.startsWith("/")) { 413 iconPath = '/' + iconPath; 414 } 415 ServletContext servletContext = (ServletContext) context.get(CallContext.SERVLET_CONTEXT); 416 if (servletContext == null) { 417 throw new CmisRuntimeException("Cannot get servlet context"); 418 } 419 return servletContext.getResourceAsStream(iconPath); 420 } 421 422 protected static List<RenditionData> getRenditionServiceRenditions(DocumentModel doc, CallContext callContext) 423 throws IOException { 424 RenditionService renditionService = Framework.getLocalService(RenditionService.class); 425 List<RenditionDefinition> defs = renditionService.getAvailableRenditionDefinitions(doc); 426 List<RenditionData> list = new ArrayList<>(defs.size()); 427 for (RenditionDefinition def : defs) { 428 if (!def.isVisible()) { 429 continue; 430 } 431 RenditionDataImpl ren = new RenditionDataImpl(); 432 String cmisName = def.getCmisName(); 433 if (StringUtils.isBlank(cmisName)) { 434 cmisName = REND_STREAM_RENDITION_PREFIX + def.getName(); 435 } 436 ren.setStreamId(cmisName); 437 String kind = def.getKind(); 438 ren.setKind(StringUtils.isNotBlank(kind) ? kind : REND_KIND_NUXEO_RENDITION); 439 ren.setTitle(def.getLabel()); 440 ren.setMimeType(def.getContentType()); 441 442 boolean computeInfo = Boolean.parseBoolean( 443 Framework.getProperty(RENDITION_COMPUTE_INFO_PROP, RENDITION_COMPUTE_INFO_DEFAULT)); 444 if (REND_KIND_CMIS_THUMBNAIL.equals(ren.getKind()) || computeInfo) { 445 Rendition rendition = renditionService.getRendition(doc, def.getName()); 446 Blob blob = rendition.getBlob(); 447 if (blob != null) { 448 ren.setTitle(blob.getFilename()); 449 SimpleImageInfo info = new SimpleImageInfo(blob.getStream()); 450 ren.setBigLength(BigInteger.valueOf(info.getLength())); 451 ren.setBigWidth(BigInteger.valueOf(info.getWidth())); 452 ren.setBigHeight(BigInteger.valueOf(info.getHeight())); 453 ren.setMimeType(info.getMimeType()); 454 } 455 } 456 list.add(ren); 457 } 458 return list; 459 } 460 461 @Override 462 public List<ObjectData> getRelationships() { 463 return getRelationships(getId(), includeRelationships, nuxeoCmisService); 464 } 465 466 public static List<ObjectData> getRelationships(String id, IncludeRelationships includeRelationships, 467 NuxeoCmisService service) { 468 if (includeRelationships == null || includeRelationships == IncludeRelationships.NONE) { 469 return null; 470 } 471 String statement = "SELECT " + PropertyIds.OBJECT_ID + ", " + PropertyIds.BASE_TYPE_ID + ", " 472 + PropertyIds.SOURCE_ID + ", " + PropertyIds.TARGET_ID + " FROM " 473 + BaseTypeId.CMIS_RELATIONSHIP.value() + " WHERE "; 474 String qid = "'" + id.replace("'", "''") + "'"; 475 if (includeRelationships != IncludeRelationships.TARGET) { 476 statement += PropertyIds.SOURCE_ID + " = " + qid; 477 } 478 if (includeRelationships == IncludeRelationships.BOTH) { 479 statement += " OR "; 480 } 481 if (includeRelationships != IncludeRelationships.SOURCE) { 482 statement += PropertyIds.TARGET_ID + " = " + qid; 483 } 484 List<ObjectData> list = new ArrayList<ObjectData>(); 485 IterableQueryResult res = null; 486 try { 487 Map<String, PropertyDefinition<?>> typeInfo = new HashMap<String, PropertyDefinition<?>>(); 488 res = service.queryAndFetch(statement, false, typeInfo); 489 for (Map<String, Serializable> map : res) { 490 list.add(service.makeObjectData(map, typeInfo)); 491 } 492 } finally { 493 if (res != null) { 494 res.close(); 495 } 496 } 497 return list; 498 } 499 500 @Override 501 public Acl getAcl() { 502 if (!Boolean.TRUE.equals(includeAcl)) { 503 return null; 504 } 505 ACP acp = doc.getACP(); 506 return getAcl(acp, false, nuxeoCmisService); 507 } 508 509 protected static Acl getAcl(ACP acp, boolean onlyBasicPermissions, NuxeoCmisService service) { 510 if (acp == null) { 511 acp = new ACPImpl(); 512 } 513 Boolean exact = Boolean.TRUE; 514 List<Ace> aces = new ArrayList<Ace>(); 515 for (ACL acl : acp.getACLs()) { 516 // inherited and non-local ACLs are non-direct 517 boolean direct = ACL.LOCAL_ACL.equals(acl.getName()); 518 Map<String, Set<String>> permissionMap = new LinkedHashMap<>(); 519 for (ACE ace : acl.getACEs()) { 520 boolean denied = ace.isDenied(); 521 String username = ace.getUsername(); 522 String permission = ace.getPermission(); 523 if (denied) { 524 if (SecurityConstants.EVERYONE.equals(username) && SecurityConstants.EVERYTHING.equals(permission)) { 525 permission = NuxeoCmisService.PERMISSION_NOTHING; 526 } else { 527 // we cannot represent this blocking 528 exact = Boolean.FALSE; 529 continue; 530 } 531 } 532 Set<String> permissions = permissionMap.get(username); 533 if (permissions == null) { 534 permissionMap.put(username, permissions = new LinkedHashSet<String>()); 535 } 536 // derive CMIS permission from Nuxeo permissions 537 boolean isBasic = false; 538 if (service.readPermissions.contains(permission)) { // Read 539 isBasic = true; 540 permissions.add(BasicPermissions.READ); 541 } 542 if (service.writePermissions.contains(permission)) { // ReadWrite 543 isBasic = true; 544 permissions.add(BasicPermissions.WRITE); 545 } 546 if (SecurityConstants.EVERYTHING.equals(permission)) { 547 isBasic = true; 548 permissions.add(BasicPermissions.ALL); 549 } 550 if (!onlyBasicPermissions) { 551 permissions.add(permission); 552 } else if (!isBasic) { 553 exact = Boolean.FALSE; 554 } 555 if (NuxeoCmisService.PERMISSION_NOTHING.equals(permission)) { 556 break; 557 } 558 } 559 for (Entry<String, Set<String>> en : permissionMap.entrySet()) { 560 String username = en.getKey(); 561 Set<String> permissions = en.getValue(); 562 if (permissions.isEmpty()) { 563 continue; 564 } 565 MutableAce entry = new AccessControlEntryImpl(); 566 entry.setPrincipal(new AccessControlPrincipalDataImpl(username)); 567 entry.setPermissions(new ArrayList<String>(permissions)); 568 entry.setDirect(direct); 569 aces.add(entry); 570 } 571 } 572 MutableAcl result = new AccessControlListImpl(); 573 result.setAces(aces); 574 result.setExact(exact); 575 return result; 576 } 577 578 @Override 579 public Boolean isExactAcl() { 580 return Boolean.FALSE; // TODO 581 } 582 583 @Override 584 public PolicyIdList getPolicyIds() { 585 if (!Boolean.TRUE.equals(includePolicyIds)) { 586 return null; 587 } 588 return new PolicyIdListImpl(); // TODO 589 } 590 591 @Override 592 public ChangeEventInfo getChangeEventInfo() { 593 return null; 594 // throw new UnsupportedOperationException(); 595 } 596 597 @Override 598 public List<CmisExtensionElement> getExtensions() { 599 return Collections.emptyList(); 600 } 601 602 @Override 603 public void setExtensions(List<CmisExtensionElement> extensions) { 604 } 605 606}