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.Set;
026import java.util.stream.Stream;
027
028import javax.jcr.Node;
029import javax.jcr.RepositoryException;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.jackrabbit.util.Text;
037
038import org.ametys.core.observation.Event;
039import org.ametys.core.observation.ObservationManager;
040import org.ametys.core.right.RightManager;
041import org.ametys.core.right.RightManager.RightResult;
042import org.ametys.core.ui.Callable;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.core.util.DateUtils;
046import org.ametys.core.util.LambdaUtils;
047import org.ametys.plugins.core.user.UserHelper;
048import org.ametys.plugins.queriesdirectory.observation.ObservationConstants;
049import org.ametys.plugins.repository.AmetysObject;
050import org.ametys.plugins.repository.AmetysObjectIterable;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.plugins.repository.AmetysRepositoryException;
053import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
054import org.ametys.plugins.repository.MovableAmetysObject;
055import org.ametys.plugins.repository.UnknownAmetysObjectException;
056import org.ametys.plugins.repository.jcr.NameHelper;
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     * Filter queries on the server side
334     * @param rootNode The root node where to seek (to refresh subtrees)
335     * @param search Textual search
336     * @param ownerOnly Only queries of the owner will be returned
337     * @param requestType Only simple/advanced requests will be returned
338     * @param solrType Only Solr requests will be returned
339     * @param scriptType Only script requests will be returned
340     * @param formattingType Only formatting will be returned
341     * @return The list of query path
342     */
343    @Callable
344    public List<String> filterQueries(String rootNode, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType)
345    {
346        List<String> matchingPaths = new ArrayList<>();
347        
348        _getMatchingQueries("root".equals(rootNode) ? getQueriesRootNode() : _resolver.resolveById(rootNode), 
349                            matchingPaths, StringUtils.stripAccents(search.toLowerCase()), ownerOnly, requestType, solrType, scriptType, formattingType);
350        
351        return matchingPaths;
352    }
353    
354    private void _getMatchingQueries(QueryContainer queryContainer, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType)
355    {
356        String containerName = StringUtils.stripAccents(queryContainer.getName().toLowerCase());
357        if (StringUtils.isNotBlank(search) && containerName.contains(search))
358        {
359            matchingPaths.add(_getQueryPath(queryContainer));
360        }
361        
362        if (!hasAnyReadableDescendant(_userProvider.getUser(), queryContainer))
363        {
364            return;
365        }
366        
367        for (AmetysObject child : queryContainer.getChildren())
368        {
369            if (child instanceof QueryContainer childQueryContainer)
370            {
371                _getMatchingQueries(childQueryContainer, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType);
372            }
373            else if (child instanceof Query childQuery)
374            {
375                _getMatchingQuery(childQuery, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType);
376            }
377        }
378    }
379    
380    private void _getMatchingQuery(Query query, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType)
381    {
382        String type = query.getType();
383        
384        if (!canRead(_userProvider.getUser(), query)
385            || formattingType && !"formatting".equals(type)
386            || requestType && !Query.Type.SIMPLE.toString().equals(type) && !Query.Type.ADVANCED.toString().equals(type)
387            || solrType && !"solr".equals(type)
388            || scriptType && !Query.Type.SCRIPT.toString().equals(type)
389            || ownerOnly && !query.getAuthor().equals(_userProvider.getUser()))
390        {
391            return;
392        }
393        
394        String queryLabel = StringUtils.stripAccents(query.getTitle().toLowerCase());
395        if (!queryLabel.contains(search))
396        {
397            return;
398        }
399        
400        // TODO tenir compte des droits
401        
402        matchingPaths.add(_getQueryPath(query));
403    }
404    
405    private String _getQueryPath(AmetysObject queryOrContainer)
406    {
407        if (queryOrContainer instanceof Query
408            || queryOrContainer instanceof QueryContainer
409                && queryOrContainer.getParent() instanceof QueryContainer)
410        {
411            return _getQueryPath(queryOrContainer.getParent()) + "#" + queryOrContainer.getId();
412        }
413        else
414        {
415            return "#root";
416        }
417    }
418
419    /**
420     * Creates a new {@link Query}
421     * @param title The title of the query
422     * @param desc The description of the query
423     * @param documentation The documentation of the query
424     * @param type The type of the query
425     * @param content The content of the query
426     * @param parentId The id of the parent of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
427     * @return A result map
428     */
429    @Callable
430    public Map<String, Object> createQuery(String title, String desc, String documentation, String type, String content, String parentId)
431    {
432        Map<String, Object> results = new HashMap<>();
433
434        QueryContainer queriesNode = _getQueryContainer(parentId);
435        
436        if (!canWrite(_userProvider.getUser(), queriesNode))
437        {
438            results.put("message", "not-allowed");
439            return results;
440        }
441
442        String name = NameHelper.filterName(title);
443        
444        // Find unique name
445        String uniqueName = name;
446        int index = 2;
447        while (queriesNode.hasChild(uniqueName))
448        {
449            uniqueName = name + "-" + (index++);
450        }
451        
452        Query query = queriesNode.createChild(uniqueName, QueryFactory.QUERY_NODETYPE);
453        query.setTitle(title);
454        query.setDescription(desc);
455        query.setDocumentation(documentation);
456        query.setAuthor(_userProvider.getUser());
457        query.setContributor(_userProvider.getUser());
458        query.setType(type);
459        query.setContent(content);
460        query.setCreationDate(ZonedDateTime.now());
461        query.setLastModificationDate(ZonedDateTime.now());
462
463        queriesNode.saveChanges();
464
465        results.put("id", query.getId());
466        results.put("title", query.getTitle());
467        results.put("content", query.getContent());
468        
469        return results;
470    }
471    
472    /**
473     * Creates a new {@link QueryContainer}
474     * @param parentId The id of the parent. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
475     * @param name The desired name for the new {@link QueryContainer}
476     * @return A result map
477     */
478    @Callable
479    public Map<String, Object> createQueryContainer(String parentId, String name)
480    {
481        QueryContainer parent = _getQueryContainer(parentId);
482
483        if (!canWrite(_userProvider.getUser(), parent))
484        {
485            return ImmutableMap.of("message", "not-allowed");
486        }
487        
488        int index = 2;
489        String legalName = Text.escapeIllegalJcrChars(name);
490        String realName = legalName;
491        while (parent.hasChild(realName))
492        {
493            realName = legalName + " (" + index + ")";
494            index++;
495        }
496        
497        QueryContainer createdChild = parent.createChild(realName, QueryContainerFactory.QUERY_CONTAINER_NODETYPE);
498        parent.saveChanges();
499        
500        return getQueryContainerProperties(createdChild);
501    }
502    
503    /**
504     * Edits a {@link Query}
505     * @param id The id of the query
506     * @param title The title of the query
507     * @param desc The description of the query
508     * @param documentation The documentation of the query
509     * @return A result map
510     */
511    @Callable
512    public Map<String, Object> updateQuery(String id, String title, String desc, String documentation)
513    {
514        Map<String, Object> results = new HashMap<>();
515        
516        Query query = _resolver.resolveById(id);
517        
518        if (canWrite(_userProvider.getUser(), query))
519        {
520            query.setTitle(title);
521            query.setDescription(desc);
522            query.setDocumentation(documentation);
523            query.setContributor(_userProvider.getUser());
524            query.setLastModificationDate(ZonedDateTime.now());
525            query.saveChanges();
526        }
527        else
528        {
529            results.put("message", "not-allowed");
530        }
531
532        results.put("id", query.getId());
533        results.put("title", query.getTitle());
534        
535        return results;
536    }
537    
538    /**
539     * Renames a {@link QueryContainer}
540     * @param id The id of the query container
541     * @param newName The new name of the container
542     * @return A result map
543     */
544    @Callable
545    public Map<String, Object> renameQueryContainer(String id, String newName)
546    {
547        QueryContainer queryContainer = _resolver.resolveById(id);
548        
549        UserIdentity currentUser = _userProvider.getUser();
550        
551        if (canRename(currentUser, queryContainer))
552        {
553            String legalName = Text.escapeIllegalJcrChars(newName);
554            Node node = queryContainer.getNode();
555            try
556            {
557                node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName);
558                node.getSession().save();
559                
560                return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id));
561            }
562            catch (RepositoryException e)
563            {
564                getLogger().error("Unable to rename query container '{}'", id, e);
565                return Map.of("message", "cannot-rename");
566            }
567        }
568        else
569        {
570            return Map.of("message", "not-allowed");
571        }
572    }
573    
574    /**
575     * Moves a {@link Query}
576     * @param id The id of the query
577     * @param newParentId The id of the new parent container of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
578     * @return A result map
579     */
580    @Callable
581    public Map<String, Object> moveQuery(String id, String newParentId)
582    {
583        Map<String, Object> results = new HashMap<>();
584        Query query = _resolver.resolveById(id);
585        QueryContainer queryContainer = _resolver.resolveById(newParentId);
586        
587        if (canDelete(_userProvider.getUser(), query) && canWrite(_userProvider.getUser(), queryContainer))
588        {
589            if (!_move(query, newParentId))
590            {
591                results.put("message", "cannot-move");
592            }
593        }
594        else
595        {
596            results.put("message", "not-allowed");
597        }
598        
599        results.put("id", query.getId());
600        return results;
601    }
602    
603    /**
604     * Moves a {@link QueryContainer}
605     * @param id The id of the query container
606     * @param newParentId The id of the new parent container of the query container. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
607     * @return A result map
608     */
609    @Callable
610    public Map<String, Object> moveQueryContainer(String id, String newParentId)
611    {
612        QueryContainer queryContainer = _resolver.resolveById(id);
613        QueryContainer parentQueryContainer = _resolver.resolveById(newParentId);
614        UserIdentity currentUser = _userProvider.getUser();
615        
616        if (canDelete(currentUser, queryContainer) && canWrite(currentUser, parentQueryContainer))
617        {
618            if (_move(queryContainer, newParentId))
619            {
620                // returns updated properties
621                return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id));
622            }
623            else
624            {
625                return Map.of("id", id, "message", "cannot-move");
626            }
627            
628        }
629        else
630        {
631            return Map.of("id", id, "message", "not-allowed");
632        }
633    }
634    
635    private boolean _move(MovableAmetysObject obj, String newParentId)
636    {
637        QueryContainer newParent = _getQueryContainer(newParentId);
638        if (obj.canMoveTo(newParent))
639        {
640            try
641            {
642                obj.moveTo(newParent, false);
643                return true;
644            }
645            catch (AmetysRepositoryException e)
646            {
647                getLogger().error("Unable to move '{}' to query container '{}'", obj.getId(), newParentId, e);
648            }
649        }
650        return false;
651    }
652    
653    private QueryContainer _getQueryContainer(String id)
654    {
655        QueryContainer container;
656        if (ROOT_QUERY_CONTAINER_ID.equals(id))
657        {
658            container = getQueriesRootNode();
659        }
660        else
661        {
662            container = _resolver.resolveById(id);
663        }
664        return container;
665    }
666    
667    /**
668     * Saves a {@link Query}
669     * @param id The id of the query
670     * @param type The type of the query
671     * @param content The content of the query
672     * @return A result map
673     */
674    @Callable
675    public Map<String, Object> saveQuery(String id, String type, String content)
676    {
677        Map<String, Object> results = new HashMap<>();
678        
679        Query query = _resolver.resolveById(id);
680        UserIdentity user = _userProvider.getUser();
681        if (canWrite(user, query))
682        {
683            query.setType(type);
684            query.setContent(content);
685            query.setContributor(user);
686            query.setLastModificationDate(ZonedDateTime.now());
687            query.saveChanges();
688        }
689        else
690        {
691            results.put("message", "not-allowed");
692        }
693
694        results.put("id", query.getId());
695        results.put("title", query.getTitle());
696        results.put("content", query.getContent());
697        
698        return results;
699    }
700    
701    /**
702     * Deletes {@link Query}(ies)
703     * @param ids The ids of the queries to delete
704     * @return A result map
705     */
706    @Callable
707    public Map<String, Object> deleteQuery(List<String> ids)
708    {
709        Map<String, Object> results = new HashMap<>();
710        
711        List<String> deletedQueries = new ArrayList<>();
712        List<String> unknownQueries = new ArrayList<>();
713        List<String> notallowedQueries = new ArrayList<>();
714         
715        for (String id : ids)
716        {
717            try
718            {
719                Query query = _resolver.resolveById(id);
720                
721                if (canDelete(_userProvider.getUser(), query))
722                {
723                    Map<String, Object> params = new HashMap<>();
724                    params.put(ObservationConstants.ARGS_QUERY_ID, query.getId());
725
726                    query.remove();
727                    query.saveChanges();
728                    deletedQueries.add(id);
729                    
730                    _observationManager.notify(new Event(ObservationConstants.EVENT_QUERY_DELETED, _userProvider.getUser(), params));
731                }
732                else
733                {
734                    notallowedQueries.add(query.getTitle());
735                }
736            }
737            catch (UnknownAmetysObjectException e)
738            {
739                unknownQueries.add(id);
740                getLogger().error("Unable to delete query. The query of id '{}' doesn't exist", id, e);
741            }
742        }
743        
744        results.put("deletedQueries", deletedQueries);
745        results.put("notallowedQueries", notallowedQueries);
746        results.put("unknownQueries", unknownQueries);
747        
748        return results;
749    }
750    
751    /**
752     * Determines if application must warn before deleting the given {@link QueryContainer}s
753     * @param ids The {@link QueryContainer} ids
754     * @return <code>true</code> if application must warn
755     */
756    @Callable
757    public boolean mustWarnBeforeDeletion(List<String> ids)
758    {
759        return ids.stream()
760                .anyMatch(LambdaUtils.wrapPredicate(this::_mustWarnBeforeDeletion));
761    }
762    
763    private boolean _mustWarnBeforeDeletion(String id)
764    {
765        QueryContainer container = _resolver.resolveById(id);
766        AmetysObjectIterable<Query> allQueries = getChildQueriesForAdministrator(container, false, List.of());
767        return _containsNotOwnQueries(allQueries);
768    }
769    
770    private boolean _containsNotOwnQueries(AmetysObjectIterable<Query> allQueries)
771    {
772        UserIdentity currentUser = _userProvider.getUser();
773        return allQueries.stream()
774                .map(Query::getAuthor)
775                .anyMatch(Predicates.not(currentUser::equals));
776    }
777
778    
779    /**
780     * Deletes {@link QueryContainer}(s)
781     * @param ids The ids of the query containers to delete
782     * @return A result map
783     */
784    @Callable
785    public Map<String, Object> deleteQueryContainer(List<String> ids)
786    {
787        Map<String, Object> results = new HashMap<>();
788        
789        List<Object> deletedQueryContainers = new ArrayList<>();
790        List<String> unknownQueryContainers = new ArrayList<>();
791        List<String> notallowedQueryContainers = new ArrayList<>();
792         
793        for (String id : ids)
794        {
795            try
796            {
797                QueryContainer queryContainer = _resolver.resolveById(id);
798                
799                if (canDelete(_userProvider.getUser(), queryContainer))
800                {
801                    Map<String, Object> props = getQueryContainerProperties(queryContainer);
802                    queryContainer.remove();
803                    queryContainer.saveChanges();
804                    deletedQueryContainers.add(props);
805                }
806                else
807                {
808                    notallowedQueryContainers.add(queryContainer.getName());
809                }
810            }
811            catch (UnknownAmetysObjectException e)
812            {
813                unknownQueryContainers.add(id);
814                getLogger().error("Unable to delete query container. The query container of id '{}' doesn't exist", id, e);
815            }
816        }
817        
818        results.put("deletedQueryContainers", deletedQueryContainers);
819        results.put("notallowedQueryContainers", notallowedQueryContainers);
820        results.put("unknownQueryContainers", unknownQueryContainers);
821        
822        return results;
823    }
824    
825    /**
826     * Gets all queries for administrator for given parent
827     * @param parent The {@link QueryContainer}, defining the context from which getting children
828     * @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
829     * @param acceptedTypes The type of queries. Can be null or empty to accept all types.
830     * @return all queries for administrator for given parent
831     */
832    public AmetysObjectIterable<Query> getChildQueriesForAdministrator(QueryContainer parent, boolean onlyDirect, List<String> acceptedTypes)
833    {
834        return _resolver.query(QueryHelper.getXPathForQueriesForAdministrator(parent, onlyDirect, acceptedTypes));
835    }
836    
837    /**
838     * Gets all queries in READ access for given parent
839     * @param parent The {@link QueryContainer}, defining the context from which getting children
840     * @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
841     * @param user The user
842     * @param acceptedTypes The type of queries. Can be null or empty to accept all types.
843     * @return all queries in READ access for given parent
844     */
845    public Stream<Query> getChildQueriesInReadAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes)
846    {
847        return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes))
848                        .stream()
849                        .filter(Query.class::isInstance)
850                        .map(obj -> (Query) obj)
851                        .filter(query -> canRead(user, query));
852    }
853
854    /**
855     * Determine if user has read access on a query
856     * @param userIdentity the user
857     * @param query the query
858     * @return true if the user have read rights on a query
859     */
860    public boolean canRead(UserIdentity userIdentity, Query query)
861    {
862        return _rightManager.hasReadAccess(userIdentity, query) || canWrite(userIdentity, query);
863    }
864    
865    /**
866     * Determines if the user has read access on a query container
867     * @param userIdentity the user
868     * @param queryContainer the query container
869     * @return true if the user has read access on the query container
870     */
871    public boolean canRead(UserIdentity userIdentity, QueryContainer queryContainer)
872    {
873        return _rightManager.hasReadAccess(userIdentity, queryContainer) || canWrite(userIdentity, queryContainer);
874    }
875
876    /**
877     * Gets all queries in WRITE access for given parent
878     * @param parent The {@link QueryContainer}, defining the context from which getting children
879     * @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
880     * @param user The user
881     * @param acceptedTypes The type of queries. Can be null or empty to accept all types.
882     * @return all queries in WRITE access for given parent
883     */
884    public Stream<Query> getChildQueriesInWriteAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes)
885    {
886        return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes))
887                .stream()
888                .filter(Query.class::isInstance)
889                .map(obj -> (Query) obj)
890                .filter(query -> canWrite(user, query));
891    }
892    
893    /**
894     * Determines if the user has write access on a query
895     * @param userIdentity the user
896     * @param query the query
897     * @return true if the user has write access on query
898     */
899    public boolean canWrite(UserIdentity userIdentity, Query query)
900    {
901        return _rightManager.hasRight(userIdentity, QUERY_HANDLE_RIGHT_ID, query) == RightResult.RIGHT_ALLOW;
902    }
903    
904    /**
905     * Determines if the user can delete a query
906     * @param userIdentity the user
907     * @param query the query
908     * @return true if the user can delete the query
909     */
910    public boolean canDelete(UserIdentity userIdentity, Query query)
911    {
912        return canWrite(userIdentity, query) && canWrite(userIdentity, (QueryContainer) query.getParent());
913    }
914    
915    /**
916     * Determines if the user can rename a query container
917     * @param userIdentity the user
918     * @param queryContainer the query container
919     * @return true if the user can delete the query
920     */
921    public boolean canRename(UserIdentity userIdentity, QueryContainer queryContainer)
922    {
923        return !_isRoot(queryContainer) && canWrite(userIdentity, queryContainer) && canWrite(userIdentity, (QueryContainer) queryContainer.getParent());
924    }
925    
926    /**
927     * Determines if the user can delete a query container
928     * @param userIdentity the user
929     * @param queryContainer the query container
930     * @return true if the user can delete the query container
931     */
932    public boolean canDelete(UserIdentity userIdentity, QueryContainer queryContainer)
933    {
934        return !_isRoot(queryContainer) // is not root
935                && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()) // has write access on parent
936                && canWrite(userIdentity, queryContainer, true); // has write access on itselft and each descendant
937    }
938    
939    /**
940     * Determines if the query container is the root node
941     * @param queryContainer the query container
942     * @return true if is root
943     */
944    protected boolean _isRoot(QueryContainer queryContainer)
945    {
946        return getQueriesRootNode().equals(queryContainer);
947    }
948
949    /**
950     * Gets all queries in WRITE access for given parent
951     * @param parent The {@link QueryContainer}, defining the context from which getting children
952     * @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
953     * @param user The user
954     * @param acceptedTypes The type of queries. Can be null or empty to accept all types.
955     * @return all queries in WRITE access for given parent
956     */
957    public Stream<Query> getChildQueriesInRightAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes)
958    {
959        return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes))
960                .stream()
961                .filter(Query.class::isInstance)
962                .map(obj -> (Query) obj)
963                .filter(query -> canAssignRights(user, query));
964    }
965
966    /**
967     * Check if a user can edit rights on a query
968     * @param userIdentity the user
969     * @param query the query
970     * @return true if the user can edit rights on a query
971     */
972    public boolean canAssignRights(UserIdentity userIdentity, Query query)
973    {
974        return canWrite(userIdentity, query) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW;
975    }
976    
977    /**
978     * Check if a user has creation rights on a query container
979     * @param userIdentity the user identity
980     * @param queryContainer the query container
981     * @return true if the user has creation rights on a query container
982     */
983    public boolean canCreate(UserIdentity userIdentity, QueryContainer queryContainer)
984    {
985        return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, "/cms") == RightResult.RIGHT_ALLOW;
986    }
987
988    
989    /**
990     * Check if a user has write access on a query container
991     * @param userIdentity the user identity
992     * @param queryContainer the query container
993     * @return true if the user has write access on the a query container
994     */
995    public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer)
996    {      
997        return canWrite(userIdentity, queryContainer, false);
998    }
999    
1000    /**
1001     * Check if a user has write access on a query container
1002     * @param userIdentity the user user identity
1003     * @param queryContainer the query container
1004     * @param recursively true to check write access on all descendants recursively
1005     * @return true if the user has write access on the a query container
1006     */
1007    public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer, boolean recursively)
1008    {      
1009        boolean hasRight = _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, queryContainer) == RightResult.RIGHT_ALLOW;
1010        if (!hasRight)
1011        {
1012            return false;
1013        }
1014           
1015        if (recursively)
1016        {
1017            try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
1018            {
1019                for (AmetysObject child : children)
1020                {
1021                    if (child instanceof QueryContainer)
1022                    {
1023                        hasRight = hasRight && canWrite(userIdentity, (QueryContainer) child, true);
1024                    }
1025                    else if (child instanceof Query)
1026                    {
1027                        hasRight = hasRight && canWrite(userIdentity, (Query) child);
1028                    }
1029                    
1030                    if (!hasRight)
1031                    {
1032                        return false;
1033                    }
1034                }
1035            }
1036        }
1037        
1038        return hasRight;
1039    }
1040    
1041    /**
1042     * Check if a user can edit rights on a query container
1043     * @param userIdentity the user
1044     * @param queryContainer the query container
1045     * @return true if the user can edit rights on a query
1046     */
1047    public boolean canAssignRights(UserIdentity userIdentity, QueryContainer queryContainer)
1048    {
1049        return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW;
1050    }
1051    
1052    /**
1053     * Gets all query containers for given parent
1054     * @param parent The {@link QueryContainer}, defining the context from which getting children
1055     * @return all query containers for given parent
1056     */
1057    public AmetysObjectIterable<QueryContainer> getChildQueryContainers(QueryContainer parent)
1058    {
1059        return _resolver.query(QueryHelper.getXPathForQueryContainers(parent));
1060    }
1061    
1062    /**
1063     * Check if a folder have a descendant in read access for a given user
1064     * @param userIdentity the user
1065     * @param queryContainer the query container
1066     * @return true if the user have read right for at least one child of this container
1067     */
1068    public boolean hasAnyReadableDescendant(UserIdentity userIdentity, QueryContainer queryContainer)
1069    {
1070        try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
1071        {
1072            for (AmetysObject child : children)
1073            {
1074                if (child instanceof QueryContainer)
1075                {
1076                    if (canRead(userIdentity, (QueryContainer) child) || hasAnyReadableDescendant(userIdentity, (QueryContainer) child))
1077                    {
1078                        return true;
1079                    }
1080                }
1081                else if (child instanceof Query && canRead(userIdentity, (Query) child))
1082                {
1083                    return true;
1084                }
1085            }
1086        }
1087        
1088        return false;
1089    }
1090
1091    /**
1092     * Check if a query container have descendant in write access for a given user
1093     * @param userIdentity the user identity
1094     * @param queryContainer the query container
1095     * @return true if the user have write right for at least one child of this container
1096     */
1097    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, QueryContainer queryContainer)
1098    {
1099        boolean canWrite = false;
1100        
1101        try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
1102        {
1103            for (AmetysObject child : children)
1104            {
1105                if (child instanceof QueryContainer)
1106                {
1107                    if (canWrite(userIdentity, (QueryContainer) child) || hasAnyWritableDescendant(userIdentity, (QueryContainer) child))
1108                    {
1109                        return true;
1110                    }
1111                }
1112                else if (child instanceof Query && canWrite(userIdentity, (Query) child))
1113                {
1114                    return true;
1115                }
1116            }
1117        }
1118        
1119        return canWrite;
1120    }
1121    
1122    /**
1123     * Check if a folder have descendant in right assignment access for a given user
1124     * @param userIdentity the user identity
1125     * @param queryContainer the query container
1126     * @return true if the user have right assignment right for at least one child of this container
1127     */
1128    public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, QueryContainer queryContainer)
1129    {
1130        try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren())
1131        {
1132            for (AmetysObject child : children)
1133            {
1134                if (child instanceof QueryContainer)
1135                {
1136                    if (canAssignRights(userIdentity, (QueryContainer) child) || hasAnyAssignableDescendant(userIdentity, (QueryContainer) child))
1137                    {
1138                        return true;
1139                    }
1140                }
1141                else if (child instanceof Query)
1142                {
1143                    if (canAssignRights(userIdentity, (Query) child))
1144                    {
1145                        return true;
1146                    }
1147                }
1148            }
1149            return false;
1150        }
1151    }
1152}