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