001/*
002 *  Copyright 2016 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.queriesdirectory;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.stream.Stream;
028
029import javax.jcr.Node;
030import javax.jcr.RepositoryException;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.jackrabbit.util.Text;
037
038import org.ametys.cms.FilterNameHelper;
039import org.ametys.core.observation.Event;
040import org.ametys.core.observation.ObservationManager;
041import org.ametys.core.right.RightManager;
042import org.ametys.core.right.RightManager.RightResult;
043import org.ametys.core.ui.Callable;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.util.DateUtils;
047import org.ametys.core.util.LambdaUtils;
048import org.ametys.plugins.core.user.UserHelper;
049import org.ametys.plugins.queriesdirectory.observation.ObservationConstants;
050import org.ametys.plugins.repository.AmetysObject;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.AmetysRepositoryException;
054import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
055import org.ametys.plugins.repository.MovableAmetysObject;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058
059import com.google.common.base.Predicates;
060import com.google.common.collect.ImmutableMap;
061
062/**
063 * DAO for manipulating queries
064 */
065public class QueryDAO extends AbstractLogEnabled implements Serviceable, Component
066{
067    /** The Avalon role */
068    public static final String ROLE = QueryDAO.class.getName();
069    
070    /** The right id to handle query */
071    public static final String QUERY_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Admin";
072    
073    /** The right id to handle query container */
074    public static final String QUERY_CONTAINER_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Containers";
075    
076    /** The alias id of the root {@link QueryContainer} */
077    public static final String ROOT_QUERY_CONTAINER_ID = "root";
078    
079    /** Propery key for read access */
080    public static final String READ_ACCESS_PROPERTY = "canRead";
081    /** Propery key for write access */
082    public static final String WRITE_ACCESS_PROPERTY = "canWrite";
083    /** Propery key for rename access */
084    public static final String RENAME_ACCESS_PROPERTY = "canRename";
085    /** Propery key for delete access */
086    public static final String DELETE_ACCESS_PROPERTY = "canDelete";
087    /** Propery key for edit rights access */
088    public static final String EDIT_RIGHTS_ACCESS_PROPERTY = "canAssignRights";
089    
090    private static final String __PLUGIN_NODE_NAME = "queriesdirectory";
091    
092    /** The current user provider */
093    protected CurrentUserProvider _userProvider;
094    
095    /** The observation manager */
096    protected ObservationManager _observationManager;
097
098    /** The Ametys object resolver */
099    private AmetysObjectResolver _resolver;
100
101    private UserHelper _userHelper;
102
103    private RightManager _rightManager;
104    
105    @Override
106    public void service(ServiceManager serviceManager) throws ServiceException
107    {
108        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
109        _userProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
110        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
111        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
112        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
113    }
114    
115    /**
116     * Get the root plugin storage object.
117     * @return the root plugin storage object.
118     * @throws AmetysRepositoryException if a repository error occurs.
119     */
120    public QueryContainer getQueriesRootNode() throws AmetysRepositoryException
121    {
122        try
123        {
124            return _getOrCreateRootNode();
125        }
126        catch (AmetysRepositoryException e)
127        {
128            throw new AmetysRepositoryException("Unable to get the queries root node", e);
129        }
130    }
131    
132    private QueryContainer _getOrCreateRootNode() throws AmetysRepositoryException
133    {
134        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins");
135        
136        ModifiableTraversableAmetysObject pluginNode = (ModifiableTraversableAmetysObject) _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured");
137        
138        return (QueryContainer) _getOrCreateNode(pluginNode, "ametys:queries", QueryContainerFactory.QUERY_CONTAINER_NODETYPE);
139    }
140    
141    private static AmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
142    {
143        AmetysObject definitionsNode;
144        if (parentNode.hasChild(nodeName))
145        {
146            definitionsNode = parentNode.getChild(nodeName);
147        }
148        else
149        {
150            definitionsNode = parentNode.createChild(nodeName, nodeType);
151            parentNode.saveChanges();
152        }
153        return definitionsNode;
154    }
155    
156    /**
157     * Get queries' properties
158     * @param queryIds The ids of queries to retrieve
159     * @return The queries' properties
160     */
161    @Callable
162    public Map<String, Object> getQueriesProperties(List<String> queryIds)
163    {
164        Map<String, Object> result = new HashMap<>();
165        
166        List<Map<String, Object>> queries = new LinkedList<>();
167        List<Map<String, Object>> notAllowedQueries = new LinkedList<>();
168        Set<String> unknownQueries = new HashSet<>();
169        
170        UserIdentity user = _userProvider.getUser();
171        
172        for (String id : queryIds)
173        {
174            try
175            {
176                Query query = _resolver.resolveById(id);
177
178                if (canRead(user, query))
179                {
180                    queries.add(getQueryProperties(query));
181                }
182                else
183                {
184                    notAllowedQueries.add(getQueryProperties(query));
185                }
186            }
187            catch (UnknownAmetysObjectException e)
188            {
189                unknownQueries.add(id);
190            }
191        }
192        
193        result.put("queries", queries);
194        result.put("unknownQueries", unknownQueries);
195        
196        return result;
197    }
198    
199    /**
200     * Get the query properties
201     * @param query The query
202     * @return The query properties
203     */
204    public Map<String, Object> getQueryProperties (Query query)
205    {
206        Map<String, Object> infos = new HashMap<>();
207        
208        List<String> fullPath = new ArrayList<>();
209        fullPath.add(query.getTitle());
210
211        AmetysObject node = query.getParent();
212        while (node instanceof QueryContainer 
213                && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root
214        {
215            fullPath.add(0, node.getName()); 
216            node = node.getParent();
217        }
218        
219        
220        infos.put("isQuery", true);
221        infos.put("id", query.getId());
222        infos.put("title", query.getTitle());
223        infos.put("fullPath", String.join(" > ", fullPath));
224        infos.put("type", query.getType());
225        infos.put("description", query.getDescription());
226        infos.put("documentation", query.getDocumentation());
227        infos.put("author", _userHelper.user2json(query.getAuthor())); 
228        infos.put("contributor", _userHelper.user2json(query.getContributor())); 
229        infos.put("content", query.getContent());
230        infos.put("lastModificationDate", DateUtils.zonedDateTimeToString(query.getLastModificationDate()));
231        infos.put("creationDate", DateUtils.zonedDateTimeToString(query.getCreationDate()));
232        
233        UserIdentity currentUser = _userProvider.getUser();
234        infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, query));
235        infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, query));
236        infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, query));
237        infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, query));
238        
239        return infos;
240    }
241    
242    /**
243     * Gets the ids of the path elements of a query or query container, i.e. the parent ids.
244     * <br>For instance, if the query path is 'a/b/c', then the result list will be ["id-of-a", "id-of-b", "id-of-c"]
245     * @param queryId The id of the query
246     * @return the ids of the path elements of a query
247     */
248    @Callable
249    public List<String> getIdsOfPath(String queryId)
250    {
251        AmetysObject queryOrQueryContainer = _resolver.resolveById(queryId);
252        QueryContainer queriesRootNode = getQueriesRootNode();
253        
254        if (!(queryOrQueryContainer instanceof Query) && !(queryOrQueryContainer instanceof QueryContainer))
255        {
256            throw new IllegalArgumentException("The given id is not a query nor a query container");
257        }
258        
259        List<String> pathElements = new ArrayList<>();
260        QueryContainer current = queryOrQueryContainer.getParent();
261        while (!queriesRootNode.equals(current))
262        {
263            pathElements.add(0, current.getId());
264            current = current.getParent();
265        }
266        
267        return pathElements;
268    }
269    
270    /**
271     * Get the root container properties
272     * @return The root container properties
273     */
274    @Callable
275    public Map<String, Object> getRootProperties()
276    {
277        return getQueryContainerProperties(getQueriesRootNode());
278    }
279    
280    /**
281     * Get the query container properties
282     * @param id The query container id. Can be {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
283     * @return The query container properties
284     */
285    @Callable
286    public Map<String, Object> getQueryContainerProperties(String id)
287    {
288        return getQueryContainerProperties(_getQueryContainer(id));
289    }
290    
291    /**
292     * Get the query container properties
293     * @param queryContainer The query container
294     * @return The query container properties
295     */
296    public Map<String, Object> getQueryContainerProperties(QueryContainer queryContainer)
297    {
298        Map<String, Object> infos = new HashMap<>();
299        
300        infos.put("isQuery", false);
301        infos.put("id", queryContainer.getId());
302        infos.put("name", queryContainer.getName());
303        infos.put("title", queryContainer.getName());
304        infos.put("fullPath", _getFullPath(queryContainer));
305        
306        UserIdentity currentUser = _userProvider.getUser();
307        
308        infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, queryContainer));
309        infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, queryContainer)); // create container, add query 
310        infos.put(RENAME_ACCESS_PROPERTY, canRename(currentUser, queryContainer)); // rename
311        infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, queryContainer)); // delete or move
312        infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, queryContainer)); // edit rights
313
314        return infos;
315    }
316    
317    private String _getFullPath(QueryContainer queryContainer)
318    {
319        List<String> fullPath = new ArrayList<>();
320        fullPath.add(queryContainer.getName());
321
322        AmetysObject node = queryContainer.getParent();
323        while (node instanceof QueryContainer 
324                && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root
325        {
326            fullPath.add(0, node.getName()); 
327            node = node.getParent();
328        }
329        return String.join(" > ", fullPath);
330    }
331    
332    /**
333     * Creates a new {@link Query}
334     * @param title The title of the query
335     * @param desc The description of the query
336     * @param documentation The documentation of the query
337     * @param type The type of the query
338     * @param content The content of the query
339     * @param parentId The id of the parent of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
340     * @return A result map
341     */
342    @Callable
343    public Map<String, Object> createQuery(String title, String desc, String documentation, String type, String content, String parentId)
344    {
345        Map<String, Object> results = new HashMap<>();
346
347        QueryContainer queriesNode = _getQueryContainer(parentId);
348        
349        if (!canWrite(_userProvider.getUser(), queriesNode))
350        {
351            results.put("message", "not-allowed");
352            return results;
353        }
354
355        String name = FilterNameHelper.filterName(title);
356        
357        // Find unique name
358        String uniqueName = name;
359        int index = 2;
360        while (queriesNode.hasChild(uniqueName))
361        {
362            uniqueName = name + "-" + (index++);
363        }
364        
365        Query query = queriesNode.createChild(uniqueName, QueryFactory.QUERY_NODETYPE);
366        query.setTitle(title);
367        query.setDescription(desc);
368        query.setDocumentation(documentation);
369        query.setAuthor(_userProvider.getUser());
370        query.setContributor(_userProvider.getUser());
371        query.setType(type);
372        query.setContent(content);
373        query.setCreationDate(ZonedDateTime.now());
374        query.setLastModificationDate(ZonedDateTime.now());
375
376        queriesNode.saveChanges();
377
378        results.put("id", query.getId());
379        results.put("title", query.getTitle());
380        results.put("content", query.getContent());
381        
382        return results;
383    }
384    
385    /**
386     * Creates a new {@link QueryContainer}
387     * @param parentId The id of the parent. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
388     * @param name The desired name for the new {@link QueryContainer}
389     * @return A result map
390     */
391    @Callable
392    public Map<String, Object> createQueryContainer(String parentId, String name)
393    {
394        QueryContainer parent = _getQueryContainer(parentId);
395
396        if (!canWrite(_userProvider.getUser(), parent))
397        {
398            return ImmutableMap.of("message", "not-allowed");
399        }
400        
401        int index = 2;
402        String legalName = Text.escapeIllegalJcrChars(name);
403        String realName = legalName;
404        while (parent.hasChild(realName))
405        {
406            realName = legalName + " (" + index + ")";
407            index++;
408        }
409        
410        QueryContainer createdChild = parent.createChild(realName, QueryContainerFactory.QUERY_CONTAINER_NODETYPE);
411        parent.saveChanges();
412        
413        return getQueryContainerProperties(createdChild);
414    }
415    
416    /**
417     * Edits a {@link Query}
418     * @param id The id of the query
419     * @param title The title of the query
420     * @param desc The description of the query
421     * @param documentation The documentation of the query
422     * @return A result map
423     */
424    @Callable
425    public Map<String, Object> updateQuery(String id, String title, String desc, String documentation)
426    {
427        Map<String, Object> results = new HashMap<>();
428        
429        Query query = _resolver.resolveById(id);
430        
431        if (canWrite(_userProvider.getUser(), query))
432        {
433            query.setTitle(title);
434            query.setDescription(desc);
435            query.setDocumentation(documentation);
436            query.setContributor(_userProvider.getUser());
437            query.setLastModificationDate(ZonedDateTime.now());
438            query.saveChanges();
439        }
440        else
441        {
442            results.put("message", "not-allowed");
443        }
444
445        results.put("id", query.getId());
446        results.put("title", query.getTitle());
447        
448        return results;
449    }
450    
451    /**
452     * Renames a {@link QueryContainer}
453     * @param id The id of the query container
454     * @param newName The new name of the container
455     * @return A result map
456     */
457    @Callable
458    public Map<String, Object> renameQueryContainer(String id, String newName)
459    {
460        QueryContainer queryContainer = _resolver.resolveById(id);
461        
462        UserIdentity currentUser = _userProvider.getUser();
463        
464        if (canRename(currentUser, queryContainer))
465        {
466            String legalName = Text.escapeIllegalJcrChars(newName);
467            Node node = queryContainer.getNode();
468            try
469            {
470                node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName);
471                node.getSession().save();
472                
473                return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id));
474            }
475            catch (RepositoryException e)
476            {
477                getLogger().error("Unable to rename query container '{}'", id, e);
478                return Map.of("message", "cannot-rename");
479            }
480        }
481        else
482        {
483            return Map.of("message", "not-allowed");
484        }
485    }
486    
487    /**
488     * Moves a {@link Query}
489     * @param id The id of the query
490     * @param newParentId The id of the new parent container of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
491     * @return A result map
492     */
493    @Callable
494    public Map<String, Object> moveQuery(String id, String newParentId)
495    {
496        Map<String, Object> results = new HashMap<>();
497        Query query = _resolver.resolveById(id);
498        QueryContainer queryContainer = _resolver.resolveById(newParentId);
499        
500        if (canDelete(_userProvider.getUser(), query) && canWrite(_userProvider.getUser(), queryContainer))
501        {
502            if (!_move(query, newParentId))
503            {
504                results.put("message", "cannot-move");
505            }
506        }
507        else
508        {
509            results.put("message", "not-allowed");
510        }
511        
512        results.put("id", query.getId());
513        return results;
514    }
515    
516    /**
517     * Moves a {@link QueryContainer}
518     * @param id The id of the query container
519     * @param newParentId The id of the new parent container of the query container. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
520     * @return A result map
521     */
522    @Callable
523    public Map<String, Object> moveQueryContainer(String id, String newParentId)
524    {
525        QueryContainer queryContainer = _resolver.resolveById(id);
526        QueryContainer parentQueryContainer = _resolver.resolveById(newParentId);
527        UserIdentity currentUser = _userProvider.getUser();
528        
529        if (canDelete(currentUser, queryContainer) && canWrite(currentUser, parentQueryContainer))
530        {
531            if (_move(queryContainer, newParentId))
532            {
533                // returns updated properties
534                return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id));
535            }
536            else
537            {
538                return Map.of("id", id, "message", "cannot-move");
539            }
540            
541        }
542        else
543        {
544            return Map.of("id", id, "message", "not-allowed");
545        }
546    }
547    
548    private boolean _move(MovableAmetysObject obj, String newParentId)
549    {
550        QueryContainer newParent = _getQueryContainer(newParentId);
551        if (obj.canMoveTo(newParent))
552        {
553            try
554            {
555                obj.moveTo(newParent, false);
556                return true;
557            }
558            catch (AmetysRepositoryException e)
559            {
560                getLogger().error("Unable to move '{}' to query container '{}'", obj.getId(), newParentId, e);
561            }
562        }
563        return false;
564    }
565    
566    private QueryContainer _getQueryContainer(String id)
567    {
568        QueryContainer container;
569        if (ROOT_QUERY_CONTAINER_ID.equals(id))
570        {
571            container = getQueriesRootNode();
572        }
573        else
574        {
575            container = _resolver.resolveById(id);
576        }
577        return container;
578    }
579    
580    /**
581     * Saves a {@link Query}
582     * @param id The id of the query
583     * @param type The type of the query
584     * @param content The content of the query
585     * @return A result map
586     */
587    @Callable
588    public Map<String, Object> saveQuery(String id, String type, String content)
589    {
590        Map<String, Object> results = new HashMap<>();
591        
592        Query query = _resolver.resolveById(id);
593        UserIdentity user = _userProvider.getUser();
594        if (canWrite(user, query))
595        {
596            query.setType(type);
597            query.setContent(content);
598            query.setContributor(user);
599            query.setLastModificationDate(ZonedDateTime.now());
600            query.saveChanges();
601        }
602        else
603        {
604            results.put("message", "not-allowed");
605        }
606
607        results.put("id", query.getId());
608        results.put("title", query.getTitle());
609        results.put("content", query.getContent());
610        
611        return results;
612    }
613    
614    /**
615     * Deletes {@link Query}(ies)
616     * @param ids The ids of the queries to delete
617     * @return A result map
618     */
619    @Callable
620    public Map<String, Object> deleteQuery(List<String> ids)
621    {
622        Map<String, Object> results = new HashMap<>();
623        
624        List<String> deletedQueries = new ArrayList<>();
625        List<String> unknownQueries = new ArrayList<>();
626        List<String> notallowedQueries = new ArrayList<>();
627         
628        for (String id : ids)
629        {
630            try
631            {
632                Query query = _resolver.resolveById(id);
633                
634                if (canDelete(_userProvider.getUser(), query))
635                {
636                    Map<String, Object> params = new HashMap<>();
637                    params.put(ObservationConstants.ARGS_QUERY_ID, query.getId());
638
639                    query.remove();
640                    query.saveChanges();
641                    deletedQueries.add(id);
642                    
643                    _observationManager.notify(new Event(ObservationConstants.EVENT_QUERY_DELETED, _userProvider.getUser(), params));
644                }
645                else
646                {
647                    notallowedQueries.add(query.getTitle());
648                }
649            }
650            catch (UnknownAmetysObjectException e)
651            {
652                unknownQueries.add(id);
653                getLogger().error("Unable to delete query. The query of id '{}' doesn't exist", id, e);
654            }
655        }
656        
657        results.put("deletedQueries", deletedQueries);
658        results.put("notallowedQueries", notallowedQueries);
659        results.put("unknownQueries", unknownQueries);
660        
661        return results;
662    }
663    
664    /**
665     * Determines if application must warn before deleting the given {@link QueryContainer}s
666     * @param ids The {@link QueryContainer} ids
667     * @return <code>true</code> if application must warn
668     */
669    @Callable
670    public boolean mustWarnBeforeDeletion(List<String> ids)
671    {
672        return ids.stream()
673                .anyMatch(LambdaUtils.wrapPredicate(this::_mustWarnBeforeDeletion));
674    }
675    
676    private boolean _mustWarnBeforeDeletion(String id)
677    {
678        QueryContainer container = _resolver.resolveById(id);
679        AmetysObjectIterable<Query> allQueries = getChildQueriesForAdministrator(container, false, Optional.empty());
680        return _containsNotOwnQueries(allQueries);
681    }
682    
683    private boolean _containsNotOwnQueries(AmetysObjectIterable<Query> allQueries)
684    {
685        UserIdentity currentUser = _userProvider.getUser();
686        return allQueries.stream()
687                .map(Query::getAuthor)
688                .anyMatch(Predicates.not(currentUser::equals));
689    }
690
691    
692    /**
693     * Deletes {@link QueryContainer}(s)
694     * @param ids The ids of the query containers to delete
695     * @return A result map
696     */
697    @Callable
698    public Map<String, Object> deleteQueryContainer(List<String> ids)
699    {
700        Map<String, Object> results = new HashMap<>();
701        
702        List<Object> deletedQueryContainers = new ArrayList<>();
703        List<String> unknownQueryContainers = new ArrayList<>();
704        List<String> notallowedQueryContainers = new ArrayList<>();
705         
706        for (String id : ids)
707        {
708            try
709            {
710                QueryContainer queryContainer = _resolver.resolveById(id);
711                
712                if (canDelete(_userProvider.getUser(), queryContainer))
713                {
714                    Map<String, Object> props = getQueryContainerProperties(queryContainer);
715                    queryContainer.remove();
716                    queryContainer.saveChanges();
717                    deletedQueryContainers.add(props);
718                }
719                else
720                {
721                    notallowedQueryContainers.add(queryContainer.getName());
722                }
723            }
724            catch (UnknownAmetysObjectException e)
725            {
726                unknownQueryContainers.add(id);
727                getLogger().error("Unable to delete query container. The query container of id '{}' doesn't exist", id, e);
728            }
729        }
730        
731        results.put("deletedQueryContainers", deletedQueryContainers);
732        results.put("notallowedQueryContainers", notallowedQueryContainers);
733        results.put("unknownQueryContainers", unknownQueryContainers);
734        
735        return results;
736    }
737    
738    /**
739     * Gets all queries for administrator for given parent
740     * @param parent The {@link QueryContainer}, defining the context from which getting children
741     * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
742     * @param type The query type
743     * @return all queries for administrator for given parent
744     */
745    public AmetysObjectIterable<Query> getChildQueriesForAdministrator(QueryContainer parent, boolean onlyDirect, Optional<String> type)
746    {
747        return _resolver.query(QueryHelper.getXPathForQueriesForAdministrator(parent, onlyDirect, type));
748    }
749    
750    /**
751     * Gets all queries in READ access for given parent
752     * @param parent The {@link QueryContainer}, defining the context from which getting children
753     * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
754     * @param user The user
755     * @param type The query type
756     * @return all queries in READ access for given parent
757     */
758    public Stream<Query> getChildQueriesInReadAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, Optional<String> type)
759    {
760        return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, type))
761                        .stream()
762                        .filter(Query.class::isInstance)
763                        .map(obj -> (Query) obj)
764                        .filter(query -> canRead(user, query));
765    }
766
767    /**
768     * Determine if user has read access on a query
769     * @param userIdentity the user
770     * @param query the query
771     * @return true if the user have read rights on a query
772     */
773    public boolean canRead(UserIdentity userIdentity, Query query)
774    {
775        return _rightManager.hasReadAccess(userIdentity, query) || canWrite(userIdentity, query);
776    }
777    
778    /**
779     * Determines if the user has read access on a query container
780     * @param userIdentity the user
781     * @param queryContainer the query container
782     * @return true if the user has read access on the query container
783     */
784    public boolean canRead(UserIdentity userIdentity, QueryContainer queryContainer)
785    {
786        return _rightManager.hasReadAccess(userIdentity, queryContainer) || canWrite(userIdentity, queryContainer);
787    }
788
789    /**
790     * Gets all queries in WRITE access for given parent
791     * @param parent The {@link QueryContainer}, defining the context from which getting children
792     * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
793     * @param user The user
794     * @param type The query type
795     * @return all queries in WRITE access for given parent
796     */
797    public Stream<Query> getChildQueriesInWriteAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, Optional<String> type)
798    {
799        return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, type))
800                .stream()
801                .filter(Query.class::isInstance)
802                .map(obj -> (Query) obj)
803                .filter(query -> canWrite(user, query));
804    }
805    
806    /**
807     * Determines if the user has write access on a query
808     * @param userIdentity the user
809     * @param query the query
810     * @return true if the user has write access on query
811     */
812    public boolean canWrite(UserIdentity userIdentity, Query query)
813    {
814        return _rightManager.hasRight(userIdentity, QUERY_HANDLE_RIGHT_ID, query) == RightResult.RIGHT_ALLOW;
815    }
816    
817    /**
818     * Determines if the user can delete a query
819     * @param userIdentity the user
820     * @param query the query
821     * @return true if the user can delete the query
822     */
823    public boolean canDelete(UserIdentity userIdentity, Query query)
824    {
825        return canWrite(userIdentity, query) && canWrite(userIdentity, (QueryContainer) query.getParent());
826    }
827    
828    /**
829     * Determines if the user can rename a query container
830     * @param userIdentity the user
831     * @param queryContainer the query container
832     * @return true if the user can delete the query
833     */
834    public boolean canRename(UserIdentity userIdentity, QueryContainer queryContainer)
835    {
836        return !_isRoot(queryContainer) && canWrite(userIdentity, queryContainer) && canWrite(userIdentity, (QueryContainer) queryContainer.getParent());
837    }
838    
839    /**
840     * Determines if the user can delete a query container
841     * @param userIdentity the user
842     * @param queryContainer the query container
843     * @return true if the user can delete the query container
844     */
845    public boolean canDelete(UserIdentity userIdentity, QueryContainer queryContainer)
846    {
847        return !_isRoot(queryContainer) // is not root
848                && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()) // has write access on parent
849                && canWrite(userIdentity, queryContainer, true); // has write access on itselft and each descendant
850    }
851    
852    /**
853     * Determines if the query container is the root node
854     * @param queryContainer the query container
855     * @return true if is root
856     */
857    protected boolean _isRoot(QueryContainer queryContainer)
858    {
859        return getQueriesRootNode().equals(queryContainer);
860    }
861
862    /**
863     * Gets all queries in WRITE access for given parent
864     * @param parent The {@link QueryContainer}, defining the context from which getting children
865     * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path
866     * @param user The user
867     * @param type The query type
868     * @return all queries in WRITE access for given parent
869     */
870    public Stream<Query> getChildQueriesInRightAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, Optional<String> type)
871    {
872        return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, type))
873                .stream()
874                .filter(Query.class::isInstance)
875                .map(obj -> (Query) obj)
876                .filter(query -> canAssignRights(user, query));
877    }
878
879    /**
880     * Check if a user can edit rights on a query
881     * @param userIdentity the user
882     * @param query the query
883     * @return true if the user can edit rights on a query
884     */
885    public boolean canAssignRights(UserIdentity userIdentity, Query query)
886    {
887        return canWrite(userIdentity, query) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW;
888    }
889    
890    /**
891     * Check if a user has creation rights on a query container
892     * @param userIdentity the user identity
893     * @param queryContainer the query container
894     * @return true if the user has creation rights on a query container
895     */
896    public boolean canCreate(UserIdentity userIdentity, QueryContainer queryContainer)
897    {
898        return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, "/cms") == RightResult.RIGHT_ALLOW;
899    }
900
901    
902    /**
903     * Check if a user has write access on a query container
904     * @param userIdentity the user identity
905     * @param queryContainer the query container
906     * @return true if the user has write access on the a query container
907     */
908    public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer)
909    {      
910        return canWrite(userIdentity, queryContainer, false);
911    }
912    
913    /**
914     * Check if a user has write access on a query container
915     * @param userIdentity the user user identity
916     * @param queryContainer the query container
917     * @param recursively true to check write access on all descendants recursively
918     * @return true if the user has write access on the a query container
919     */
920    public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer, boolean recursively)
921    {      
922        boolean hasRight = _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, queryContainer) == RightResult.RIGHT_ALLOW;
923        if (!hasRight)
924        {
925            return false;
926        }
927           
928        if (recursively)
929        {
930            try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
931            {
932                for (AmetysObject child : children)
933                {
934                    if (child instanceof QueryContainer)
935                    {
936                        hasRight = hasRight && canWrite(userIdentity, (QueryContainer) child, true);
937                    }
938                    else if (child instanceof Query)
939                    {
940                        hasRight = hasRight && canWrite(userIdentity, (Query) child);
941                    }
942                    
943                    if (!hasRight)
944                    {
945                        return false;
946                    }
947                }
948            }
949        }
950        
951        return hasRight;
952    }
953    
954    /**
955     * Check if a user can edit rights on a query container
956     * @param userIdentity the user
957     * @param queryContainer the query container
958     * @return true if the user can edit rights on a query
959     */
960    public boolean canAssignRights(UserIdentity userIdentity, QueryContainer queryContainer)
961    {
962        return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW;
963    }
964    
965    /**
966     * Gets all query containers for given parent
967     * @param parent The {@link QueryContainer}, defining the context from which getting children
968     * @return all query containers for given parent
969     */
970    public AmetysObjectIterable<QueryContainer> getChildQueryContainers(QueryContainer parent)
971    {
972        return _resolver.query(QueryHelper.getXPathForQueryContainers(parent));
973    }
974    
975    /**
976     * Check if a folder have a descendant in read access for a given user
977     * @param userIdentity the user
978     * @param queryContainer the query container
979     * @return true if the user have read right for at least one child of this container
980     */
981    public boolean hasAnyReadableDescendant(UserIdentity userIdentity, QueryContainer queryContainer)
982    {
983        try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
984        {
985            for (AmetysObject child : children)
986            {
987                if (child instanceof QueryContainer)
988                {
989                    if (canRead(userIdentity, (QueryContainer) child) || hasAnyReadableDescendant(userIdentity, (QueryContainer) child))
990                    {
991                        return true;
992                    }
993                }
994                else if (child instanceof Query && canRead(userIdentity, (Query) child))
995                {
996                    return true;
997                }
998            }
999        }
1000        
1001        return false;
1002    }
1003
1004    /**
1005     * Check if a query container have descendant in write access for a given user
1006     * @param userIdentity the user identity
1007     * @param queryContainer the query container
1008     * @return true if the user have write right for at least one child of this container
1009     */
1010    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, QueryContainer queryContainer)
1011    {
1012        boolean canWrite = false;
1013        
1014        try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
1015        {
1016            for (AmetysObject child : children)
1017            {
1018                if (child instanceof QueryContainer)
1019                {
1020                    if (canWrite(userIdentity, (QueryContainer) child) || hasAnyWritableDescendant(userIdentity, (QueryContainer) child))
1021                    {
1022                        return true;
1023                    }
1024                }
1025                else if (child instanceof Query && canWrite(userIdentity, (Query) child))
1026                {
1027                    return true;
1028                }
1029            }
1030        }
1031        
1032        return canWrite;
1033    }
1034    
1035    /**
1036     * Check if a folder have descendant in right assignment access for a given user
1037     * @param userIdentity the user identity
1038     * @param queryContainer the query container
1039     * @return true if the user have right assignment right for at least one child of this container
1040     */
1041    public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, QueryContainer queryContainer)
1042    {
1043        try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
1044        {
1045            for (AmetysObject child : children)
1046            {
1047                if (child instanceof QueryContainer)
1048                {
1049                    if (canAssignRights(userIdentity, (QueryContainer) child) || hasAnyAssignableDescendant(userIdentity, (QueryContainer) child))
1050                    {
1051                        return true;
1052                    }
1053                }
1054                else if (child instanceof Query)
1055                {
1056                    if (canAssignRights(userIdentity, (Query) child))
1057                    {
1058                        return true;
1059                    }
1060                }
1061            }
1062            return false;
1063        }
1064    }
1065}