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