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