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