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}