001/*
002 *  Copyright 2020 Anyware Services
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 */
016package org.ametys.plugins.repository.jcr;
017
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import javax.jcr.Node;
026import javax.jcr.NodeIterator;
027import javax.jcr.PathNotFoundException;
028import javax.jcr.Repository;
029import javax.jcr.RepositoryException;
030import javax.jcr.Session;
031import javax.jcr.Value;
032import javax.jcr.lock.Lock;
033import javax.jcr.lock.LockManager;
034import javax.jcr.query.Query;
035
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.jackrabbit.util.ISO9075;
041import org.apache.jackrabbit.util.Text;
042import org.slf4j.Logger;
043
044import org.ametys.core.group.GroupIdentity;
045import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys;
046import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.util.LambdaUtils;
049import org.ametys.plugins.repository.ACLAmetysObject;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.AmetysRepositoryException;
052import org.ametys.plugins.repository.ModifiableACLAmetysObject;
053import org.ametys.plugins.repository.ModifiableACLAmetysObjectProfileAssignmentStorage;
054import org.ametys.plugins.repository.RepositoryConstants;
055import org.ametys.plugins.repository.provider.AbstractRepository;
056import org.ametys.plugins.repository.query.expression.Expression;
057import org.ametys.plugins.repository.query.expression.OrExpression;
058import org.ametys.runtime.plugin.component.LogEnabled;
059
060/**
061 * Helper for implementing {@link ModifiableACLAmetysObject} in JCR under its node.
062 */
063public class ACLJCRAmetysObjectHelper implements Component, Serviceable, LogEnabled
064{
065    /** The AmetysObject resolver */
066    protected static AmetysObjectResolver _resolver;
067    /** The repository */
068    protected static Repository _repository;
069    
070    private static final String __NODE_NAME_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":acl";
071    private static final String __NODETYPE_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX + ":acl";
072    
073    private static final String __NODE_NAME_ACL_USERS = "users";
074    private static final String __NODE_NAME_ACL_GROUPS = "groups";
075    private static final String __NODETYPE_ACL_USER = RepositoryConstants.NAMESPACE_PREFIX + ":acl-user";
076    private static final String __NODETYPE_ACL_GROUP = RepositoryConstants.NAMESPACE_PREFIX + ":acl-group";
077    private static final String __NODETYPE_UNSTRUCTURED = "nt:unstructured";
078    
079    private static final String __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-any-connected-profiles";
080    private static final String __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-any-connected-profiles";
081    private static final String __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-anonymous-profiles";
082    private static final String __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-anonymous-profiles";
083    
084    private static final String __PROPERTY_NAME_ALLOWED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles";
085    private static final String __PROPERTY_NAME_DENIED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-profiles";
086    
087    private static final String __PROPERTY_NAME_DISALLOW_INHERITANCE = RepositoryConstants.NAMESPACE_PREFIX + ":disallow-inheritance";
088    
089    private static final Map<AnonymousOrAnyConnectedKeys, Set<String>> __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT = Map.of(
090            AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, Set.of(),
091            AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, Set.of(),
092            AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, Set.of(),
093            AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, Set.of());
094    private static final Map<UserOrGroup, Set<String>> __USER_OR_GROUP_NORIGHT = Map.of(
095            UserOrGroup.ALLOWED, Set.of(),
096            UserOrGroup.DENIED, Set.of());
097 
098    
099    private static Logger _logger;
100    
101    @Override
102    public void service(ServiceManager manager) throws ServiceException
103    {
104        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
105        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
106    }
107    
108    public void setLogger(Logger logger)
109    {
110        _logger = logger;
111    }
112    
113    
114    /* -------------- */
115    /* HAS PERMISSION */
116    /* -------------- */
117    
118    private static Set<String> _convertNodeToPath(Set<? extends Object> rootNodes)
119    {
120        return rootNodes.stream().filter(JCRAmetysObject.class::isInstance).map(JCRAmetysObject.class::cast).map(LambdaUtils.wrap(ao -> ISO9075.encodePath(ao.getNode().getPath()))).collect(Collectors.toSet());
121    }
122    
123    
124    /**
125     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for the user
126     * @param user The user
127     * @param profileIds The ids of the profiles to check
128     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
129     * @return If the Set is empty, it means the user has no matching profile.<br>
130     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for the user AND it can contains some other profiles that were not in the given profiles
131     */
132    public static Set<String> hasUserAnyAllowedProfile(Set<? extends Object> rootNodes, UserIdentity user, Set<String> profileIds)
133    {
134        Expression expr = new AllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
135        for (String rootPath : _convertNodeToPath(rootNodes))
136        {
137            NodeIterator nodes = getACLUsers(user, rootPath, expr);
138            
139            if (nodes.hasNext())
140            {
141                // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
142                Node userNode = nodes.nextNode();
143                return _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
144            }
145        }
146        return Set.of();
147    }
148    
149    /**
150     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for the group
151     * @param groups The groups
152     * @param profileIds The ids of the profiles
153     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
154     * @return If the Set is empty, it means the group has no matching profile.<br>
155     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for the group AND it can contains some other profiles that were not in the given profiles
156     */
157    public static Set<String> hasGroupAnyAllowedProfile(Set<? extends Object> rootNodes, Set<GroupIdentity> groups, Set<String> profileIds)
158    {
159        if (!groups.isEmpty())
160        {
161            Expression expr = new AllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
162            for (String rootPath : _convertNodeToPath(rootNodes))
163            {
164                // Approximative query (to be fast)
165                NodeIterator nodes = _getApprochingACLGroups(groups, rootPath, expr);
166                
167                while (nodes.hasNext())
168                {
169                    Node groupNode = nodes.nextNode();
170    
171                    // As the query was a fast approximative request, we now check if the result is fine
172                    String groupId;
173                    String directoryId;
174                    try
175                    {
176                        groupId = Text.unescapeIllegalJcrChars(groupNode.getName());
177                        directoryId = groupNode.getParent().getName();
178                    }
179                    catch (RepositoryException ex)
180                    {
181                        throw new AmetysRepositoryException("An error occured getting group information", ex);
182                    }
183                    
184                    if (groups.contains(new GroupIdentity(groupId, directoryId)))
185                    {
186                        // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
187                        return _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
188                    }
189                }
190            }
191        }
192        
193        return Set.of();
194    }
195    
196    /**
197     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for any connected user
198     * @param profileIds The ids of the profiles
199     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
200     * @return If the Set is empty, it means any connected user has no matching profile.<br>
201     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for anyconnected user AND it can contains some other profiles that were not in the given profiles
202     */
203    public static Set<String> hasAnyConnectedAnyAllowedProfile(Set<? extends Object> rootNodes, Set<String> profileIds)
204    {
205        Expression expr = new AnyConnectedAllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
206        for (String rootPath : _convertNodeToPath(rootNodes))
207        {
208            NodeIterator nodes = getACLRoots(rootPath, expr);
209            
210            if (nodes.hasNext())
211            {
212                // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
213                Node aclNode = nodes.nextNode();
214                return _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES);
215            }
216        }
217        return Set.of();
218    }
219    
220    
221    /**
222     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for anonymous
223     * @param profileIds The ids of the profiles
224     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
225     * @return If the Set is empty, it means anonymous has no matching profile.<br>
226     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for anonymous AND it can contains some other profiles that were not in the given profiles
227     */
228    public static Set<String> hasAnonymousAnyAllowedProfile(Set<? extends Object> rootNodes, Set<String> profileIds)
229    {
230        Expression expr = new AnonymousAllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
231        for (String rootPath : _convertNodeToPath(rootNodes))
232        {
233            NodeIterator nodes = getACLRoots(rootPath, expr);
234            
235            if (nodes.hasNext())
236            {
237                // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
238                Node aclNode = nodes.nextNode();
239                return _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES);
240            }
241        }
242        return Set.of();
243    }
244    
245    /**
246     * Returns all ACL root objects (ametys:acl nodes)
247     * @param rootPath The root path to restrict the search. Can be null.
248     * @param predicat The predicat expression. Can be null.
249     * @return The ACL root objects
250     */
251    public static NodeIterator getACLRoots (String rootPath, Expression predicat)
252    {
253        StringBuilder sb = new StringBuilder("/jcr:root");
254        
255        if (rootPath != null)
256        {
257            sb.append(rootPath);
258        }
259        
260        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")");
261        
262        if (predicat != null)
263        {
264            sb.append("[").append(predicat.build()).append("]");
265        }
266        
267        return _query(sb.toString());
268    }
269    
270    /**
271     * Returns all ACL objects for a given user (ametys:acl-user nodes)
272     * @param user The user
273     * @param rootPath The root path to restrict the search. Can be null.
274     * @param predicat The predicat expression. Can be null.
275     * @return The ACL user objects for user
276     */
277    public static NodeIterator getACLUsers (UserIdentity user, String rootPath, Expression predicat)
278    {
279        StringBuilder sb = new StringBuilder("/jcr:root");
280        
281        if (rootPath != null)
282        {
283            sb.append(rootPath);
284        }
285        
286        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")")
287            .append("/").append(__NODE_NAME_ACL_USERS)
288            .append("/").append(user.getPopulationId())
289            .append("/").append(ISO9075.encode(user.getLogin()));
290        
291        if (predicat != null)
292        {
293            sb.append("[").append(predicat.build()).append("]");
294        }
295        
296        String jcrQuery = sb.toString();
297        return _query(jcrQuery);
298    }
299    
300    /**
301     * Returns all ACL objects for users (ametys:acl-user nodes)
302     * @param predicat The predicat expression. Can be null.
303     * @return The ACL user objects for users
304     */
305    public static NodeIterator getACLUsers (Expression predicat)
306    {
307        StringBuilder sb = new StringBuilder();
308        
309        sb.append("//element(*, ").append(__NODETYPE_ACL_USER).append(")");
310        
311        if (predicat != null)
312        {
313            sb.append("[").append(predicat.build()).append("]");
314        }
315        
316        return _query(sb.toString());
317    }
318    
319    /**
320     * Returns all ACL objects for groups (ametys:acl-group nodes)
321     * @param predicat The predicat expression. Can be null.
322     * @return The ACL group objects for groups
323     */
324    public static NodeIterator getACLGroups (Expression predicat)
325    {
326        StringBuilder sb = new StringBuilder();
327        
328        sb.append("//element(*, ").append(__NODETYPE_ACL_GROUP).append(")");
329        
330        if (predicat != null)
331        {
332            sb.append("[").append(predicat.build()).append("]");
333        }
334        
335        return _query(sb.toString());
336    }
337    
338    private static NodeIterator _getApprochingACLGroups (Set<GroupIdentity> groups, String rootPath, Expression predicat)
339    {
340        StringBuilder sb = new StringBuilder("/jcr:root");
341        
342        if (rootPath != null)
343        {
344            sb.append(rootPath);
345        }
346        
347        sb.append("//element(*, ametys:acl-group)[(");
348        
349        sb.append(groups.stream()
350            .map(GroupIdentity::getId)
351            .map(Text::escapeIllegalJcrChars)
352            .map(nodeName -> "fn:name()='" + nodeName + "'")
353            .collect(Collectors.joining(" or ")));
354        sb.append(")");
355        
356        if (predicat != null)
357        {
358            sb.append(" and ").append(predicat.build());
359        }
360        
361        sb.append("]");
362        
363        return _query(sb.toString());
364    }
365    
366    /**
367     * Returns all ACL objects for a given group (ametys:acl-group nodes)
368     * @param group The group
369     * @param rootPath The root path to restrict the search. Can be null.
370     * @param predicat The predicat expression. Can be null.
371     * @return The ACL user objects for groups
372     */
373    public static NodeIterator getACLGroups (GroupIdentity group, String rootPath, Expression predicat)
374    {
375        StringBuilder sb = new StringBuilder("/jcr:root");
376        
377        if (rootPath != null)
378        {
379            sb.append(rootPath);
380        }
381        
382        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")")
383            .append("/").append(__NODE_NAME_ACL_GROUPS)
384            .append("/").append(group.getDirectoryId())
385            .append("/").append(ISO9075.encode(Text.escapeIllegalJcrChars(group.getId())));
386        
387        if (predicat != null)
388        {
389            sb.append("[").append(predicat.build()).append("]");
390        }
391        
392        return _query(sb.toString());
393    }
394    
395    private static NodeIterator _query (String jcrQuery)
396    {
397        Session session = null;
398        try
399        {
400            session = _repository.login();
401            long t1 = System.currentTimeMillis();
402            @SuppressWarnings("deprecation")
403            Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
404            if (_logger.isInfoEnabled())
405            {
406                _logger.info("ACLJCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms");
407            }
408            return query.execute().getNodes();
409        }
410        catch (RepositoryException ex)
411        {
412            if (session != null)
413            {
414                session.logout();
415            }
416            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
417        }
418    }
419    
420    /* ------------------------------------------- */
421    /* PROFILES FOR ANY CONNECTED USER / ANONYMOUS */
422    /* ------------------------------------------- */
423    
424    /**
425     * Helper for {@link ACLAmetysObject#getProfilesForAnonymousAndAnyConnectedUser}
426     * @param node The JCR node for the Ametys object
427     * @return a map containing allowed/denied profiles that anonymous and any connected user has on the given object
428     */
429    public static Map<AnonymousOrAnyConnectedKeys, Set<String>> getProfilesForAnonymousAndAnyConnectedUser(Node node)
430    {
431        Node aclNode = _getACLNode(node);
432        if (aclNode == null)
433        {
434            return __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT;
435        }
436        else
437        {
438            return Map.of(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED,         _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES),
439                          AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED,          _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES),
440                          AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED,  _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES),
441                          AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED,   _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES));
442        }
443    }
444
445    /**
446     * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnyConnectedUser(Set)}
447     * @param node The JCR node for the Ametys object
448     * @param profileIds The profiles to add
449     */
450    public static void addAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
451    {
452        Node aclNode = _getOrCreateACLNode(node);
453        for (String profile : profileIds)
454        {
455            _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile);
456        }
457        _save(node);
458    }
459
460    /**
461     * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnyConnectedUser(Set)}
462     * @param node The JCR node for the Ametys object
463     * @param profileIds The profiles to remove
464     */
465    public static void removeAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
466    {
467        Node aclNode = _getOrCreateACLNode(node);
468        for (String profile : profileIds)
469        {
470            _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile);
471        }
472        _save(node);
473    }
474    
475    /**
476     * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)}
477     * @param node The JCR node for the Ametys object
478     * @param profileIds The profiles to add
479     */
480    public static void addDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
481    {
482        Node aclNode = _getOrCreateACLNode(node);
483        for (String profile : profileIds)
484        {
485            _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile);
486        }
487        _save(node);
488    }
489    
490    /**
491     * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)}
492     * @param node The JCR node for the Ametys object
493     * @param profileIds The profiles to remove
494     */
495    public static void removeDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
496    {
497        Node aclNode = _getOrCreateACLNode(node);
498        for (String profile : profileIds)
499        {
500            _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile);
501        }
502        _save(node);
503    }
504    
505    /**
506     * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnonymous(Set)}
507     * @param node The JCR node for the Ametys object
508     * @param profileIds The profiles to add
509     */
510    public static void addAllowedProfilesForAnonymous(Node node, Set<String> profileIds)
511    {
512        Node aclNode = _getOrCreateACLNode(node);
513        for (String profile : profileIds)
514        {
515            _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile);
516        }
517        _save(node);
518    }
519    
520    /**
521     * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnonymous(Set)}
522     * @param node The JCR node for the Ametys object
523     * @param profileIds The profiles to remove
524     */
525    public static void removeAllowedProfilesForAnonymous(Node node, Set<String> profileIds)
526    {
527        Node aclNode = _getOrCreateACLNode(node);
528        for (String profile : profileIds)
529        {
530            _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile);
531        }
532        _save(node);
533    }
534    
535    /**
536     * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)}
537     * @param node The JCR node for the Ametys object
538     * @param profileIds The profiles to add
539     */
540    public static void addDeniedProfilesForAnonymous(Node node, Set<String> profileIds)
541    {
542        Node aclNode = _getOrCreateACLNode(node);
543        for (String profile : profileIds)
544        {
545            _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile);
546        }
547        _save(node);
548    }
549    
550    /**
551     * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)}
552     * @param node The JCR node for the Ametys object
553     * @param profileIds The profiles to remove
554     */
555    public static void removeDeniedProfilesForAnonymous(Node node, Set<String> profileIds)
556    {
557        Node aclNode = _getOrCreateACLNode(node);
558        for (String profile : profileIds)
559        {
560            _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile);
561        }
562        _save(node);
563    }
564    
565    
566    /* ------------------- */
567    /* MANAGEMENT OF USERS */
568    /* ------------------- */
569    /**
570     * Helper for {@link ACLAmetysObject#getProfilesForUsers}
571     * @param node The JCR node for the Ametys object
572     * @param user The user to get profiles for. Can be null to get profiles for all users that have rights
573     * @return The map of allowed users with their assigned allowed/denied profiles
574     */
575    public static Map<UserIdentity, Map<UserOrGroup, Set<String>>> getProfilesForUsers(Node node, UserIdentity user)
576    {
577        if (user == null)
578        {
579            try
580            {
581                Node usersNode = _getUsersNode(node);
582                if (usersNode == null)
583                {
584                    return Map.of();
585                }
586                
587                Map<UserIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
588                
589                NodeIterator populationsIterator = usersNode.getNodes();
590                while (populationsIterator.hasNext())
591                {
592                    Node populationNode = populationsIterator.nextNode();
593                    NodeIterator usersIterator = populationNode.getNodes();
594                    while (usersIterator.hasNext())
595                    {
596                        Node userNode = usersIterator.nextNode();
597                        Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
598                        Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES);
599                        if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty())
600                        {
601                            result.put(new UserIdentity(userNode.getName(), populationNode.getName()), 
602                                        Map.of(UserOrGroup.ALLOWED, allowedProfiles,
603                                               UserOrGroup.DENIED, deniedProfiles));
604                        }
605                    }
606                }
607                
608                return result;
609            }
610            catch (RepositoryException e)
611            {
612                throw new AmetysRepositoryException("Unable to get allowed/denied users", e);
613            }
614        }
615        else
616        {
617            Node userNode = _getUserNode(node, user);
618            if (userNode == null)
619            {
620                return Map.of(user, __USER_OR_GROUP_NORIGHT);
621            }
622            else
623            {
624                return Map.of(user, 
625                              Map.of(UserOrGroup.ALLOWED, _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES),
626                                     UserOrGroup.DENIED,  _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES)));
627            }
628        }
629    }
630
631    /**
632     * Helper for {@link ModifiableACLAmetysObject#addAllowedUsers(Set, String)}
633     * @param users The users to add
634     * @param node The JCR node for the Ametys object
635     * @param profileId The id of the profile
636     */
637    public static void addAllowedUsers(Set<UserIdentity> users, Node node, String profileId)
638    {
639        for (UserIdentity userIdentity : users)
640        {
641            Node userNode = _getOrCreateUserNode(node, userIdentity);
642            _addProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
643        }
644        _save(node);
645    }
646
647    /**
648     * Helper for {@link ModifiableACLAmetysObject#removeAllowedUsers(Set, String)}
649     * @param users The users to remove
650     * @param node The JCR node for the Ametys object
651     * @param profileId The id of the profile
652     */
653    public static void removeAllowedUsers(Set<UserIdentity> users, Node node, String profileId)
654    {
655        for (UserIdentity userIdentity : users)
656        {
657            Node userNode = _getOrCreateUserNode(node, userIdentity);
658            _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
659        }
660        _save(node);
661    }
662
663    /**
664     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)}
665     * @param users The users to remove
666     * @param node The JCR node for the Ametys object
667     */
668    public static void removeAllowedUsers(Set<UserIdentity> users, Node node)
669    {
670        for (UserIdentity userIdentity : users)
671        {
672            Node userNode = _getOrCreateUserNode(node, userIdentity);
673            _setProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET);
674        } 
675        _save(node);
676    }
677    
678    /**
679     * Helper for {@link ModifiableACLAmetysObject#addDeniedUsers(Set, String)}
680     * @param users The users to add
681     * @param node The JCR node for the Ametys object
682     * @param profileId The id of the profile
683     */
684    public static void addDeniedUsers(Set<UserIdentity> users, Node node, String profileId)
685    {
686        for (UserIdentity userIdentity : users)
687        {
688            Node userNode = _getOrCreateUserNode(node, userIdentity);
689            _addProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
690        }
691        _save(node);
692    }
693
694    /**
695     * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set, String)}
696     * @param users The users to remove
697     * @param node The JCR node for the Ametys object
698     * @param profileId The id of the profile
699     */
700    public static void removeDeniedUsers(Set<UserIdentity> users, Node node, String profileId)
701    {
702        for (UserIdentity userIdentity : users)
703        {
704            Node userNode = _getOrCreateUserNode(node, userIdentity);
705            _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
706        }
707        _save(node);
708    }
709
710    /**
711     * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set)}
712     * @param users The users to remove
713     * @param node The JCR node for the Ametys object
714     */
715    public static void removeDeniedUsers(Set<UserIdentity> users, Node node)
716    {
717        for (UserIdentity userIdentity : users)
718        {
719            Node userNode = _getOrCreateUserNode(node, userIdentity);
720            _setProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET);
721        } 
722        _save(node);
723    }
724
725    /* -------------------- */
726    /* MANAGEMENT OF GROUPS */
727    /* -------------------- */
728
729    /**
730     * Helper for {@link ACLAmetysObject#getProfilesForGroups} 
731     * @param node The JCR node for the Ametys object
732     * @param groups The group to get profiles for. Can be null to get profiles for all groups that have rights
733     * @return The map of allowed/denied groups with their assigned profiles
734     */
735    public static Map<GroupIdentity, Map<UserOrGroup, Set<String>>> getProfilesForGroups(Node node, Set<GroupIdentity> groups)
736    {
737        try
738        {
739            if (groups != null && groups.isEmpty())
740            {
741                return Map.of();
742            }
743            
744            Node groupsNode = _getGroupsNode(node);
745            if (groupsNode == null)
746            {
747                return Map.of();
748            }
749            
750            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
751            
752            if (groups == null)
753            {
754                NodeIterator groupDirectoriesIterator = groupsNode.getNodes();
755                while (groupDirectoriesIterator.hasNext())
756                {
757                    Node groupDirectoryNode = groupDirectoriesIterator.nextNode();
758                    NodeIterator groupsIterator = groupDirectoryNode.getNodes();
759                    while (groupsIterator.hasNext())
760                    {
761                        Node groupNode = groupsIterator.nextNode();
762                        Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
763                        Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES);
764                        if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty())
765                        {
766                            result.put(new GroupIdentity(Text.unescapeIllegalJcrChars(groupNode.getName()), groupDirectoryNode.getName()), 
767                                        Map.of(UserOrGroup.ALLOWED, allowedProfiles,
768                                               UserOrGroup.DENIED, deniedProfiles));
769                        }
770                    }
771                }
772            }
773            else
774            {
775                Map<String, Node> groupsNodeByDirectoryIdCache = new HashMap<>();
776                
777                for (GroupIdentity group : groups)
778                {
779                    Node directoryNode = groupsNodeByDirectoryIdCache.computeIfAbsent(group.getDirectoryId(), LambdaUtils.wrap(directoryId -> groupsNode.hasNode(directoryId) ? groupsNode.getNode(directoryId) : null));
780                    
781                    String groupNodeName = Text.escapeIllegalJcrChars(group.getId());
782                    if (directoryNode != null && directoryNode.hasNode(groupNodeName))
783                    {
784                        Node groupNode = directoryNode.getNode(groupNodeName);
785                        
786                        result.put(group, 
787                                Map.of(UserOrGroup.ALLOWED, _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES),
788                                       UserOrGroup.DENIED, _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES)));
789                    }
790                }
791            }
792            
793            return result;
794        }
795        catch (RepositoryException e)
796        {
797            throw new AmetysRepositoryException("Unable to get allowed/denied groups", e);
798        }
799    }
800    
801    /**
802     * Helper for {@link ModifiableACLAmetysObject#addAllowedGroups(Set, String)}
803     * @param groups The groups to add
804     * @param node The JCR node for the Ametys object
805     * @param profileId The id of the profile
806     */
807    public static void addAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId)
808    {
809        for (GroupIdentity groupIdentity : groups)
810        {
811            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
812            _addProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
813        }
814        _save(node);
815    }
816
817    /**
818     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set, String)}
819     * @param groups The groups to remove
820     * @param node The JCR node for the Ametys object
821     * @param profileId The id of the profile
822     */
823    public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId)
824    {
825        for (GroupIdentity groupIdentity : groups)
826        {
827            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
828            _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
829        }
830        _save(node);
831    }
832
833    /**
834     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)}
835     * @param groups The groups to remove
836     * @param node The JCR node for the Ametys object
837     */
838    public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node)
839    {
840        for (GroupIdentity groupIdentity : groups)
841        {
842            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
843            _setProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET);
844        } 
845        _save(node);
846    }
847
848    /**
849     * Helper for {@link ModifiableACLAmetysObject#addDeniedGroups(Set, String)}
850     * @param groups The groups to add
851     * @param node The JCR node for the Ametys object
852     * @param profileId The id of the profile
853     */
854    public static void addDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId)
855    {
856        for (GroupIdentity groupIdentity : groups)
857        {
858            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
859            _addProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
860        }
861        _save(node);
862    }
863
864    /**
865     * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set, String)}
866     * @param groups The groups to remove
867     * @param node The JCR node for the Ametys object
868     * @param profileId The id of the profile
869     */
870    public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId)
871    {
872        for (GroupIdentity groupIdentity : groups)
873        {
874            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
875            _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
876        }
877        _save(node);
878    }
879
880    /**
881     * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set)}
882     * @param groups The groups to remove
883     * @param node The JCR node for the Ametys object
884     */
885    public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node)
886    {
887        for (GroupIdentity groupIdentity : groups)
888        {
889            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
890            _setProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET);
891        } 
892        _save(node);
893    }
894    
895    
896    /* ------ */
897    /* REMOVE */
898    /* ------ */
899    
900    /**
901     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeProfile(String)}
902     * @param profileId The id of the profile
903     */
904    public static void removeProfile(String profileId)
905    {
906        // Remove this profile set as allowed or denied in users 
907        Expression expr = new OrExpression(new AllowedProfileExpression(profileId), new DeniedProfileExpression(profileId));
908        NodeIterator users = getACLUsers(expr);
909        while (users.hasNext())
910        {
911            Node userNode = (Node) users.next();
912            _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
913            _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
914            _save(userNode);
915        }
916        
917        // Remove this profile set as allowed or denied in groups 
918        NodeIterator groups = getACLGroups(expr);
919        while (groups.hasNext())
920        {
921            Node groupNode = (Node) groups.next();
922            _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
923            _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
924            _save(groupNode);
925        }
926        
927        // Remove this profile set as allowed or denied for anonymous and any connected 
928        expr = new OrExpression(new AnonymousAllowedProfileExpression(profileId), new AnonymousDeniedProfileExpression(profileId), new AnyConnectedAllowedProfileExpression(profileId), new AnyConnectedDeniedProfileExpression(profileId));
929        NodeIterator nodes = getACLRoots(null, expr);
930        while (nodes.hasNext())
931        {
932            Node node = (Node) nodes.next();
933            _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileId);
934            _removeProperty(node, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileId);
935            _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileId);
936            _removeProperty(node, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileId);
937            _save(node);
938        }
939    }
940    
941    /**
942     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeUser(UserIdentity)}
943     * @param user The user
944     */
945    public static void removeUser(UserIdentity user)
946    {
947        NodeIterator users = getACLUsers(user, null, null);
948        
949        while (users.hasNext())
950        {
951            Node userNode = (Node) users.next();
952            try
953            {
954                userNode.remove();
955                _save(userNode);
956            }
957            catch (RepositoryException e)
958            {
959                throw new AmetysRepositoryException(e);
960            }
961        }
962    }
963    
964    /**
965     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeGroup(GroupIdentity)}
966     * @param group The group
967     */
968    public static void removeGroup(GroupIdentity group)
969    {
970        NodeIterator groups = getACLGroups(group, null, null);
971        while (groups.hasNext())
972        {
973            Node gpNode = (Node) groups.next();
974            try
975            {
976                gpNode.remove();
977                _save(gpNode);
978            }
979            catch (RepositoryException e)
980            {
981                throw new AmetysRepositoryException(e);
982            }
983        }
984    }
985    
986    /* --------------- */
987    /* INHERITANCE     */
988    /* --------------- */
989    /**
990     * Helper for {@link ACLAmetysObject#isInheritanceDisallowed()}
991     * @param node The JCR node for the Ametys object
992     * @return true if the inheritance is disallow of the given node
993     */
994    public static boolean isInheritanceDisallowed(Node node)
995    {
996        try
997        {
998            Node aclNode = _getACLNode(node);
999            if (aclNode != null && aclNode.hasProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE))
1000            {
1001                return aclNode.getProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE).getBoolean();
1002            }
1003            return false;
1004        }
1005        catch (RepositoryException e)
1006        {
1007            throw new AmetysRepositoryException("Unable to get " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e);
1008        }
1009    }
1010    
1011    /**
1012     * Helper for {@link ModifiableACLAmetysObject#disallowInheritance(boolean)}
1013     * @param node The JCR node for the Ametys object
1014     * @param disallow true to disallow the inheritance, false otherwise
1015     */
1016    public static void disallowInheritance(Node node, boolean disallow)
1017    {
1018        Node aclNode = _getOrCreateACLNode(node);
1019        try
1020        {
1021            aclNode.setProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE, disallow);
1022        }
1023        catch (RepositoryException e)
1024        {
1025            throw new AmetysRepositoryException("Unable to set " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e);
1026        }
1027        _save(node);
1028    }
1029    
1030    
1031    /* --------------- */
1032    /* PRIVATE METHODS */
1033    /* --------------- */
1034    
1035    private static void _checkLock(Node node) throws AmetysRepositoryException
1036    {
1037        try
1038        {
1039            if (node.isLocked())
1040            {
1041                LockManager lockManager = node.getSession().getWorkspace().getLockManager();
1042                
1043                Lock lock = lockManager.getLock(node.getPath());
1044                Node lockHolder = lock.getNode();
1045                
1046                lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
1047            }
1048        }
1049        catch (RepositoryException e)
1050        {
1051            throw new AmetysRepositoryException("Unable to add lock token on ACL node", e);
1052        }
1053    }
1054    
1055    private static Node _getOrCreateACLNode(Node node)
1056    {
1057        try
1058        {
1059            if (node.hasNode(__NODE_NAME_ROOT_ACL))
1060            {
1061                return node.getNode(__NODE_NAME_ROOT_ACL);
1062            }
1063            else
1064            {
1065                _checkLock(node);
1066                return node.addNode(__NODE_NAME_ROOT_ACL, __NODETYPE_ROOT_ACL);
1067            }
1068        }
1069        catch (RepositoryException e)
1070        {
1071            throw new AmetysRepositoryException("Error while getting root ACL node.", e);
1072        }
1073    }
1074    
1075    private static Node _getACLNode(Node node)
1076    {
1077        try
1078        {
1079            if (node.hasNode(__NODE_NAME_ROOT_ACL))
1080            {
1081                return node.getNode(__NODE_NAME_ROOT_ACL);
1082            }
1083            else
1084            {
1085                return null;
1086            }
1087        }
1088        catch (RepositoryException e)
1089        {
1090            throw new AmetysRepositoryException("Error while getting root ACL node.", e);
1091        }
1092    }
1093    
1094    private static Node _getOrCreateUsersNode(Node node)
1095    {
1096        try
1097        {
1098            Node aclNode = _getOrCreateACLNode(node);
1099            if (aclNode.hasNode(__NODE_NAME_ACL_USERS))
1100            {
1101                return aclNode.getNode(__NODE_NAME_ACL_USERS);
1102            }
1103            else
1104            {
1105                return aclNode.addNode(__NODE_NAME_ACL_USERS, __NODETYPE_UNSTRUCTURED);
1106            }
1107        }
1108        catch (RepositoryException e)
1109        {
1110            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1111        }
1112    }
1113    
1114    private static Node _getUserNode(Node node, UserIdentity user)
1115    {
1116        try
1117        {
1118            Node aclNode = _getACLNode(node);
1119            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS))
1120            {
1121                Node aclUsersNode = aclNode.getNode(__NODE_NAME_ACL_USERS);
1122                if (aclUsersNode.hasNode(user.getPopulationId()))
1123                {
1124                    Node popNode = aclUsersNode.getNode(user.getPopulationId());
1125                    if (popNode.hasNode(user.getLogin()))
1126                    {
1127                        return popNode.getNode(user.getLogin());
1128                    }
1129                }
1130            }
1131            
1132            return null;
1133        }
1134        catch (RepositoryException e)
1135        {
1136            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1137        }
1138    }
1139    
1140    private static Node _getUsersNode(Node node)
1141    {
1142        try
1143        {
1144            Node aclNode = _getACLNode(node);
1145            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS))
1146            {
1147                return aclNode.getNode(__NODE_NAME_ACL_USERS);
1148            }
1149            else
1150            {
1151                return null;
1152            }
1153        }
1154        catch (RepositoryException e)
1155        {
1156            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1157        }
1158    }
1159    
1160    private static Node _getOrCreateGroupsNode(Node node)
1161    {
1162        try
1163        {
1164            Node aclNode = _getOrCreateACLNode(node);
1165            if (aclNode.hasNode(__NODE_NAME_ACL_GROUPS))
1166            {
1167                return aclNode.getNode(__NODE_NAME_ACL_GROUPS);
1168            }
1169            else
1170            {
1171                return aclNode.addNode(__NODE_NAME_ACL_GROUPS, __NODETYPE_UNSTRUCTURED);
1172            }
1173        }
1174        catch (RepositoryException e)
1175        {
1176            throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e);
1177        }
1178    }
1179    
1180    private static Node _getGroupsNode(Node node)
1181    {
1182        try
1183        {
1184            Node aclNode = _getACLNode(node);
1185            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_GROUPS))
1186            {
1187                return aclNode.getNode(__NODE_NAME_ACL_GROUPS);
1188            }
1189            else
1190            {
1191                return null;
1192            }
1193        }
1194        catch (RepositoryException e)
1195        {
1196            throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e);
1197        }
1198    }
1199    
1200    private static Node _getOrCreateUserNode(Node node, UserIdentity userIdentity)
1201    {
1202        try
1203        {
1204            Node usersNode = _getOrCreateUsersNode(node);
1205            String population = userIdentity.getPopulationId();
1206            String login = userIdentity.getLogin();
1207            
1208            if (usersNode.hasNode(population))
1209            {
1210                Node populationNode = usersNode.getNode(population);
1211                if (populationNode.hasNode(login))
1212                {
1213                    return populationNode.getNode(login);
1214                }
1215                else
1216                {
1217                    return populationNode.addNode(login, __NODETYPE_ACL_USER);
1218                }
1219            }
1220            else
1221            {
1222                return usersNode.addNode(population, __NODETYPE_UNSTRUCTURED).addNode(login, __NODETYPE_ACL_USER);
1223            }
1224        }
1225        catch (RepositoryException e)
1226        {
1227            throw new AmetysRepositoryException(String.format("Error while getting 'user' ACL node for %s.", userIdentity.toString()), e);
1228        }
1229    }
1230    
1231    private static Node _getOrCreateGroupNode(Node node, GroupIdentity groupIdentity)
1232    {
1233        try
1234        {
1235            Node groupsNode = _getOrCreateGroupsNode(node);
1236            String directoryId = groupIdentity.getDirectoryId();
1237            String id = Text.escapeIllegalJcrChars(groupIdentity.getId());
1238            
1239            if (groupsNode.hasNode(directoryId))
1240            {
1241                Node populationNode = groupsNode.getNode(directoryId);
1242                if (populationNode.hasNode(id))
1243                {
1244                    return populationNode.getNode(id);
1245                }
1246                else
1247                {
1248                    return populationNode.addNode(id, __NODETYPE_ACL_GROUP);
1249                }
1250            }
1251            else
1252            {
1253                return groupsNode.addNode(directoryId, __NODETYPE_UNSTRUCTURED).addNode(id, __NODETYPE_ACL_GROUP);
1254            }
1255        }
1256        catch (RepositoryException e)
1257        {
1258            throw new AmetysRepositoryException(String.format("Error while getting 'group' ACL node for %s.", groupIdentity.toString()), e);
1259        }
1260    }
1261    
1262    private static Set<String> _getProperty(Node node, String propertyName)
1263    {
1264        try
1265        {
1266            Value[] values = node.getProperty(propertyName).getValues();
1267            Set<String> result = new HashSet<>();
1268            for (Value value : values)
1269            {
1270                result.add(value.getString());
1271            }
1272            return result;
1273        }
1274        catch (PathNotFoundException e)
1275        {
1276            return new HashSet<>();
1277        }
1278        catch (RepositoryException e)
1279        {
1280            throw new AmetysRepositoryException("Unable to get " + propertyName + " property", e);
1281        }
1282    }
1283    
1284    private static void _setProperty(Node node, String propertyName, Set<String> profiles)
1285    {
1286        try
1287        {
1288            node.setProperty(propertyName, profiles.toArray(new String[profiles.size()]));
1289        }
1290        catch (RepositoryException e)
1291        {
1292            throw new AmetysRepositoryException("Unable to set " + propertyName + " property", e);
1293        }
1294    }
1295    
1296    private static void _addProperty(Node node, String propertyName, String profileToAdd)
1297    {
1298        Set<String> profiles = _getProperty(node, propertyName);
1299        if (!profiles.contains(profileToAdd))
1300        {
1301            profiles.add(profileToAdd);
1302            _setProperty(node, propertyName, profiles);
1303        }
1304    }
1305    
1306    private static void _removeProperty(Node node, String propertyName, String profileToRemove)
1307    {
1308        Set<String> profiles = _getProperty(node, propertyName);
1309        if (profiles.contains(profileToRemove))
1310        {
1311            profiles.remove(profileToRemove);
1312            _setProperty(node, propertyName, profiles);
1313        }
1314    }
1315    
1316    private static void _save(Node node)
1317    {
1318        try
1319        {
1320            node.getSession().save();
1321        }
1322        catch (RepositoryException e)
1323        {
1324            throw new AmetysRepositoryException("Unable to save changes", e);
1325        }
1326    }
1327    
1328    /* ---------------------------------------*/
1329    /*      JCR EXPRESSIONS FOR PROFILES      */
1330    /* ---------------------------------------*/
1331    
1332    static class AllowedProfileExpression extends ACLProfileExpression
1333    {
1334        public AllowedProfileExpression (String ... profileIds)
1335        {
1336            super(__PROPERTY_NAME_ALLOWED_PROFILES, profileIds);
1337        }
1338    }
1339    
1340    static class DeniedProfileExpression extends ACLProfileExpression
1341    {
1342        public DeniedProfileExpression (String ... profileIds)
1343        {
1344            super(__PROPERTY_NAME_DENIED_PROFILES, profileIds);
1345        }
1346    }
1347    
1348    static class AnyConnectedDeniedProfileExpression extends ACLProfileExpression
1349    {
1350        public AnyConnectedDeniedProfileExpression (String ... profileIds)
1351        {
1352            super(__PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileIds);
1353        }
1354    }
1355    
1356    static class AnyConnectedAllowedProfileExpression extends ACLProfileExpression
1357    {
1358        public AnyConnectedAllowedProfileExpression (String ... profileIds)
1359        {
1360            super(__PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileIds);
1361        }
1362    }
1363    
1364    static class AnonymousDeniedProfileExpression extends ACLProfileExpression
1365    {
1366        public AnonymousDeniedProfileExpression (String ... profileIds)
1367        {
1368            super(__PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileIds);
1369        }
1370    }
1371    
1372    static class AnonymousAllowedProfileExpression extends ACLProfileExpression
1373    {
1374        public AnonymousAllowedProfileExpression (String ... profileIds)
1375        {
1376            super(__PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileIds);
1377        }
1378    }
1379    
1380    static class ACLProfileExpression implements Expression
1381    {
1382        private String[] _profileIds;
1383        private String _propertyName;
1384        
1385        public ACLProfileExpression (String propertyName, String ... profileIds)
1386        {
1387            _propertyName = propertyName;
1388            _profileIds = profileIds;
1389        }
1390        
1391        @Override
1392        public String build()
1393        {
1394            boolean isFirst = true;
1395            StringBuilder sb = new StringBuilder("(");
1396            
1397            for (String profileId : _profileIds)
1398            {
1399                if (isFirst)
1400                {
1401                    isFirst = false;
1402                }
1403                else
1404                {
1405                    sb.append(" or ");
1406                }
1407                
1408                sb.append("@")
1409                    .append(_propertyName)
1410                    .append(Operator.EQ)
1411                    .append("'").append(profileId).append("'");
1412            }
1413            
1414            if (isFirst)
1415            {
1416                return "";
1417            }
1418            else
1419            {
1420                return sb.append(")").toString();
1421            }
1422        }
1423    }
1424}