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.authentication.AccessDeniedException;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059
060import com.google.common.base.Predicates;
061import com.google.common.collect.ImmutableMap;
062
063/**
064 * DAO for manipulating queries
065 */
066public class QueryDAO extends AbstractLogEnabled implements Serviceable, Component
067{
068    /** The Avalon role */
069    public static final String ROLE = QueryDAO.class.getName();
070    
071    /** The right id to handle query */
072    public static final String QUERY_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Admin";
073    
074    /** The right id to handle query container */
075    public static final String QUERY_CONTAINER_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Containers";
076    
077    /** The alias id of the root {@link QueryContainer} */
078    public static final String ROOT_QUERY_CONTAINER_ID = "root";
079    
080    /** Propery key for read access */
081    public static final String READ_ACCESS_PROPERTY = "canRead";
082    /** Propery key for write access */
083    public static final String WRITE_ACCESS_PROPERTY = "canWrite";
084    /** Propery key for rename access */
085    public static final String RENAME_ACCESS_PROPERTY = "canRename";
086    /** Propery key for delete access */
087    public static final String DELETE_ACCESS_PROPERTY = "canDelete";
088    /** Propery key for edit rights access */
089    public static final String EDIT_RIGHTS_ACCESS_PROPERTY = "canAssignRights";
090    
091    private static final String __PLUGIN_NODE_NAME = "queriesdirectory";
092    
093    /** The current user provider */
094    protected CurrentUserProvider _userProvider;
095    
096    /** The observation manager */
097    protected ObservationManager _observationManager;
098
099    /** The Ametys object resolver */
100    private AmetysObjectResolver _resolver;
101
102    private UserHelper _userHelper;
103
104    private RightManager _rightManager;
105    
106    @Override
107    public void service(ServiceManager serviceManager) throws ServiceException
108    {
109        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
110        _userProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
111        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
112        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
113        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
114    }
115    
116    /**
117     * Get the root plugin storage object.
118     * @return the root plugin storage object.
119     * @throws AmetysRepositoryException if a repository error occurs.
120     */
121    public QueryContainer getQueriesRootNode() throws AmetysRepositoryException
122    {
123        try
124        {
125            return _getOrCreateRootNode();
126        }
127        catch (AmetysRepositoryException e)
128        {
129            throw new AmetysRepositoryException("Unable to get the queries root node", e);
130        }
131    }
132    
133    private QueryContainer _getOrCreateRootNode() throws AmetysRepositoryException
134    {
135        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins");
136        
137        ModifiableTraversableAmetysObject pluginNode = (ModifiableTraversableAmetysObject) _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured");
138        
139        return (QueryContainer) _getOrCreateNode(pluginNode, "ametys:queries", QueryContainerFactory.QUERY_CONTAINER_NODETYPE);
140    }
141    
142    private static AmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
143    {
144        AmetysObject definitionsNode;
145        if (parentNode.hasChild(nodeName))
146        {
147            definitionsNode = parentNode.getChild(nodeName);
148        }
149        else
150        {
151            definitionsNode = parentNode.createChild(nodeName, nodeType);
152            parentNode.saveChanges();
153        }
154        return definitionsNode;
155    }
156    
157    /**
158     * Get queries' properties
159     * @param queryIds The ids of queries to retrieve
160     * @return The queries' properties
161     */
162    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
163    public Map<String, Object> getQueriesProperties(List<String> queryIds)
164    {
165        Map<String, Object> result = new HashMap<>();
166        
167        List<Map<String, Object>> queries = new LinkedList<>();
168        List<String> notAllowedQueries = new LinkedList<>();
169        Set<String> unknownQueries = new HashSet<>();
170        
171        UserIdentity user = _userProvider.getUser();
172        
173        for (String id : queryIds)
174        {
175            try
176            {
177                Query query = _resolver.resolveById(id);
178                
179                if (canRead(user, query))
180                {
181                    queries.add(getQueryProperties(query));
182                }
183                else
184                {
185                    notAllowedQueries.add(query.getId());
186                }
187            }
188            catch (UnknownAmetysObjectException e)
189            {
190                unknownQueries.add(id);
191            }
192        }
193        
194        result.put("queries", queries);
195        result.put("notAllowedQueries", notAllowedQueries);
196        result.put("unknownQueries", unknownQueries);
197        
198        return result;
199    }
200    
201    /**
202     * Get the query properties
203     * @param query The query
204     * @return The query properties
205     */
206    public Map<String, Object> getQueryProperties (Query query)
207    {
208        Map<String, Object> infos = new HashMap<>();
209        
210        List<String> fullPath = new ArrayList<>();
211        fullPath.add(query.getTitle());
212
213        AmetysObject node = query.getParent();
214        while (node instanceof QueryContainer
215                && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root
216        {
217            fullPath.add(0, node.getName());
218            node = node.getParent();
219        }
220        
221        
222        infos.put("isQuery", true);
223        infos.put("id", query.getId());
224        infos.put("title", query.getTitle());
225        infos.put("fullPath", String.join(" > ", fullPath));
226        infos.put("type", query.getType());
227        infos.put("description", query.getDescription());
228        infos.put("documentation", query.getDocumentation());
229        infos.put("author", _userHelper.user2json(query.getAuthor()));
230        infos.put("contributor", _userHelper.user2json(query.getContributor()));
231        infos.put("content", query.getContent());
232        infos.put("lastModificationDate", DateUtils.zonedDateTimeToString(query.getLastModificationDate()));
233        infos.put("creationDate", DateUtils.zonedDateTimeToString(query.getCreationDate()));
234        
235        UserIdentity currentUser = _userProvider.getUser();
236        infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, query));
237        infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, query));
238        infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, query));
239        infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, query));
240        
241        return infos;
242    }
243    
244    /**
245     * Gets the ids of the path elements of a query or query container, i.e. the parent ids.
246     * <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"]
247     * @param queryId The id of the query
248     * @return the ids of the path elements of a query
249     */
250    @Callable (rights = {QUERY_CONTAINER_HANDLE_RIGHT_ID, QUERY_HANDLE_RIGHT_ID}, rightContext = QueriesDirectoryRightAssignmentContext.ID, paramIndex = 0)
251    public List<String> getIdsOfPath(String queryId)
252    {
253        AmetysObject queryOrQueryContainer = _resolver.resolveById(queryId);
254        QueryContainer queriesRootNode = getQueriesRootNode();
255        
256        if (!(queryOrQueryContainer instanceof Query) && !(queryOrQueryContainer instanceof QueryContainer))
257        {
258            throw new IllegalArgumentException("The given id is not a query nor a query container");
259        }
260        
261        List<String> pathElements = new ArrayList<>();
262        QueryContainer current = queryOrQueryContainer.getParent();
263        while (!queriesRootNode.equals(current))
264        {
265            pathElements.add(0, current.getId());
266            current = current.getParent();
267        }
268        
269        return pathElements;
270    }
271    
272    /**
273     * Get the root container properties
274     * @return The root container properties
275     */
276    @Callable(rights = {"QueriesDirectory_Rights_Tool", "CMS_Rights_Delegate_Rights", "Runtime_Rights_Rights_Handle"})
277    public Map<String, Object> getRootProperties()
278    {
279        return getQueryContainerProperties(getQueriesRootNode());
280    }
281    
282    /**
283     * Get the query container properties
284     * @param id The query container id. Can be {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
285     * @return The query container properties
286     */
287    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
288    public Map<String, Object> getQueryContainerProperties(String id)
289    {
290        UserIdentity user = _userProvider.getUser();
291        QueryContainer container = _getQueryContainer(id);
292        if (!canRead(user, container))
293        {
294            throw new AccessDeniedException(user + " tried to access the properties of container " + id + " without sufficients rights");
295        }
296        return getQueryContainerProperties(container);
297    }
298    
299    /**
300     * Get the query container properties
301     * @param queryContainer The query container
302     * @return The query container properties
303     */
304    public Map<String, Object> getQueryContainerProperties(QueryContainer queryContainer)
305    {
306        Map<String, Object> infos = new HashMap<>();
307        
308        infos.put("isQuery", false);
309        infos.put("id", queryContainer.getId());
310        infos.put("name", queryContainer.getName());
311        infos.put("title", queryContainer.getName());
312        infos.put("fullPath", _getFullPath(queryContainer));
313        
314        UserIdentity currentUser = _userProvider.getUser();
315        
316        infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, queryContainer));
317        infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, queryContainer)); // create container, add query
318        infos.put(RENAME_ACCESS_PROPERTY, canRename(currentUser, queryContainer)); // rename
319        infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, queryContainer)); // delete or move
320        infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, queryContainer)); // edit rights
321
322        return infos;
323    }
324    
325    private String _getFullPath(QueryContainer queryContainer)
326    {
327        List<String> fullPath = new ArrayList<>();
328        fullPath.add(queryContainer.getName());
329
330        AmetysObject node = queryContainer.getParent();
331        while (node instanceof QueryContainer
332                && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root
333        {
334            fullPath.add(0, node.getName());
335            node = node.getParent();
336        }
337        return String.join(" > ", fullPath);
338    }
339    
340    /**
341     * Filter queries on the server side
342     * @param rootNode The root node where to seek (to refresh subtrees)
343     * @param search Textual search
344     * @param ownerOnly Only queries of the owner will be returned
345     * @param requestType Only simple/advanced requests will be returned
346     * @param solrType Only Solr requests will be returned
347     * @param scriptType Only script requests will be returned
348     * @param formattingType Only formatting will be returned
349     * @return The list of query path
350     */
351    @Callable (rights = {"QueriesDirectory_Rights_Tool", "CMS_Rights_Delegate_Rights", "Runtime_Rights_Rights_Handle"})
352    public List<String> filterQueries(String rootNode, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType)
353    {
354        List<String> matchingPaths = new ArrayList<>();
355        _getMatchingQueries(_getQueryContainer(rootNode),
356                            matchingPaths, StringUtils.stripAccents(search.toLowerCase()), ownerOnly, requestType, solrType, scriptType, formattingType);
357        
358        return matchingPaths;
359    }
360    
361    private void _getMatchingQueries(QueryContainer queryContainer, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType)
362    {
363        if (StringUtils.isBlank(search))
364        {
365            return;
366        }
367        
368        String containerName = StringUtils.stripAccents(queryContainer.getName().toLowerCase());
369        if (containerName.contains(search))
370        {
371            matchingPaths.add(_getQueryPath(queryContainer));
372        }
373        
374        if (!hasAnyReadableDescendant(_userProvider.getUser(), queryContainer))
375        {
376            return;
377        }
378        
379        try (AmetysObjectIterable<AmetysObject> children  = queryContainer.getChildren())
380        {
381            for (AmetysObject child : children)
382            {
383                if (child instanceof QueryContainer childQueryContainer)
384                {
385                    _getMatchingQueries(childQueryContainer, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType);
386                }
387                else if (child instanceof Query childQuery)
388                {
389                    _getMatchingQuery(childQuery, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType);
390                }
391            }
392        }
393    }
394    
395    private void _getMatchingQuery(Query query, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType)
396    {
397        String type = query.getType();
398        
399        if (!canRead(_userProvider.getUser(), query)
400            || formattingType && !"formatting".equals(type)
401            || requestType && !Query.Type.SIMPLE.toString().equals(type) && !Query.Type.ADVANCED.toString().equals(type)
402            || solrType && !"solr".equals(type)
403            || scriptType && !Query.Type.SCRIPT.toString().equals(type)
404            || ownerOnly && !query.getAuthor().equals(_userProvider.getUser()))
405        {
406            return;
407        }
408        
409        if (_contains(query, search))
410        {
411            matchingPaths.add(_getQueryPath(query));
412        }
413    }
414    
415    /**
416     * Check it the query contains the search string in its title, content, description or documentation
417     */
418    private boolean _contains(Query query, String search)
419    {
420        if (StringUtils.stripAccents(query.getTitle().toLowerCase()).contains(search))
421        {
422            return true;
423        }
424        
425        if (StringUtils.stripAccents(query.getContent().toLowerCase()).contains(search))
426        {
427            return true;
428        }
429        
430        if (StringUtils.stripAccents(query.getDescription().toLowerCase()).contains(search))
431        {
432            return true;
433        }
434        return false;
435    }
436    
437    private String _getQueryPath(AmetysObject queryOrContainer)
438    {
439        if (queryOrContainer instanceof Query
440            || queryOrContainer instanceof QueryContainer
441                && queryOrContainer.getParent() instanceof QueryContainer)
442        {
443            return _getQueryPath(queryOrContainer.getParent()) + "#" + queryOrContainer.getId();
444        }
445        else
446        {
447            return "#root";
448        }
449    }
450
451    /**
452     * Creates a new {@link Query}
453     * @param title The title of the query
454     * @param desc The description of the query
455     * @param documentation The documentation of the query
456     * @param type The type of the query
457     * @param content The content of the query
458     * @param parentId The id of the parent of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
459     * @return A result map
460     */
461    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
462    public Map<String, Object> createQuery(String title, String desc, String documentation, String type, String content, String parentId)
463    {
464        Map<String, Object> results = new HashMap<>();
465
466        QueryContainer queriesNode = _getQueryContainer(parentId);
467        
468        if (!canWrite(_userProvider.getUser(), queriesNode))
469        {
470            results.put("message", "not-allowed");
471            return results;
472        }
473
474        String name = NameHelper.filterName(title);
475        
476        // Find unique name
477        String uniqueName = name;
478        int index = 2;
479        while (queriesNode.hasChild(uniqueName))
480        {
481            uniqueName = name + "-" + (index++);
482        }
483        
484        Query query = queriesNode.createChild(uniqueName, QueryFactory.QUERY_NODETYPE);
485        query.setTitle(title);
486        query.setDescription(desc);
487        query.setDocumentation(documentation);
488        query.setAuthor(_userProvider.getUser());
489        query.setContributor(_userProvider.getUser());
490        query.setType(type);
491        query.setContent(content);
492        query.setCreationDate(ZonedDateTime.now());
493        query.setLastModificationDate(ZonedDateTime.now());
494
495        queriesNode.saveChanges();
496
497        results.put("id", query.getId());
498        results.put("title", query.getTitle());
499        results.put("content", query.getContent());
500        
501        return results;
502    }
503    
504    /**
505     * Creates a new {@link QueryContainer}
506     * @param parentId The id of the parent. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
507     * @param name The desired name for the new {@link QueryContainer}
508     * @return A result map
509     */
510    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
511    public Map<String, Object> createQueryContainer(String parentId, String name)
512    {
513        QueryContainer parent = _getQueryContainer(parentId);
514
515        if (!canWrite(_userProvider.getUser(), parent))
516        {
517            return ImmutableMap.of("message", "not-allowed");
518        }
519        
520        int index = 2;
521        String legalName = Text.escapeIllegalJcrChars(name);
522        String realName = legalName;
523        while (parent.hasChild(realName))
524        {
525            realName = legalName + " (" + index + ")";
526            index++;
527        }
528        
529        QueryContainer createdChild = parent.createChild(realName, QueryContainerFactory.QUERY_CONTAINER_NODETYPE);
530        parent.saveChanges();
531        
532        return getQueryContainerProperties(createdChild);
533    }
534    
535    /**
536     * Edits a {@link Query}
537     * @param id The id of the query
538     * @param title The title of the query
539     * @param desc The description of the query
540     * @param documentation The documentation of the query
541     * @return A result map
542     */
543    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
544    public Map<String, Object> updateQuery(String id, String title, String desc, String documentation)
545    {
546        Map<String, Object> results = new HashMap<>();
547        
548        Query query = _resolver.resolveById(id);
549        
550        if (canWrite(_userProvider.getUser(), query))
551        {
552            query.setTitle(title);
553            query.setDescription(desc);
554            query.setDocumentation(documentation);
555            query.setContributor(_userProvider.getUser());
556            query.setLastModificationDate(ZonedDateTime.now());
557            query.saveChanges();
558        }
559        else
560        {
561            results.put("message", "not-allowed");
562        }
563
564        results.put("id", query.getId());
565        results.put("title", query.getTitle());
566        
567        return results;
568    }
569    
570    /**
571     * Renames a {@link QueryContainer}
572     * @param id The id of the query container
573     * @param newName The new name of the container
574     * @return A result map
575     */
576    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
577    public Map<String, Object> renameQueryContainer(String id, String newName)
578    {
579        QueryContainer queryContainer = _resolver.resolveById(id);
580        
581        UserIdentity currentUser = _userProvider.getUser();
582        
583        if (canRename(currentUser, queryContainer))
584        {
585            String legalName = Text.escapeIllegalJcrChars(newName);
586            Node node = queryContainer.getNode();
587            try
588            {
589                node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName);
590                node.getSession().save();
591                
592                return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id));
593            }
594            catch (RepositoryException e)
595            {
596                getLogger().error("Unable to rename query container '{}'", id, e);
597                return Map.of("message", "cannot-rename");
598            }
599        }
600        else
601        {
602            return Map.of("message", "not-allowed");
603        }
604    }
605    
606    /**
607     * Moves a {@link Query}
608     * @param id The id of the query
609     * @param newParentId The id of the new parent container of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
610     * @return A result map
611     */
612    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
613    public Map<String, Object> moveQuery(String id, String newParentId)
614    {
615        Map<String, Object> results = new HashMap<>();
616        Query query = _resolver.resolveById(id);
617        QueryContainer queryContainer = _getQueryContainer(newParentId);
618        
619        if (canDelete(_userProvider.getUser(), query) && canWrite(_userProvider.getUser(), queryContainer))
620        {
621            if (!_move(query, newParentId))
622            {
623                results.put("message", "cannot-move");
624            }
625        }
626        else
627        {
628            results.put("message", "not-allowed");
629        }
630        
631        results.put("id", query.getId());
632        return results;
633    }
634    
635    /**
636     * Moves a {@link QueryContainer}
637     * @param id The id of the query container
638     * @param newParentId The id of the new parent container of the query container. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container.
639     * @return A result map
640     */
641    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
642    public Map<String, Object> moveQueryContainer(String id, String newParentId)
643    {
644        QueryContainer queryContainer = _resolver.resolveById(id);
645        QueryContainer parentQueryContainer = _getQueryContainer(newParentId);
646        UserIdentity currentUser = _userProvider.getUser();
647        
648        if (canDelete(currentUser, queryContainer) && canWrite(currentUser, parentQueryContainer))
649        {
650            if (_move(queryContainer, newParentId))
651            {
652                // returns updated properties
653                return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id));
654            }
655            else
656            {
657                return Map.of("id", id, "message", "cannot-move");
658            }
659            
660        }
661        else
662        {
663            return Map.of("id", id, "message", "not-allowed");
664        }
665    }
666    
667    private boolean _move(MovableAmetysObject obj, String newParentId)
668    {
669        QueryContainer newParent = _getQueryContainer(newParentId);
670        if (obj.canMoveTo(newParent))
671        {
672            try
673            {
674                obj.moveTo(newParent, false);
675                return true;
676            }
677            catch (AmetysRepositoryException e)
678            {
679                getLogger().error("Unable to move '{}' to query container '{}'", obj.getId(), newParentId, e);
680            }
681        }
682        return false;
683    }
684    
685    private QueryContainer _getQueryContainer(String id)
686    {
687        return ROOT_QUERY_CONTAINER_ID.equals(id)
688                ? getQueriesRootNode()
689                : _resolver.resolveById(id);
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 (rights = Callable.CHECKED_BY_IMPLEMENTATION)
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            results.put("id", query.getId());
715            results.put("content", query.getContent());
716            results.put("title", query.getTitle());
717        }
718        else
719        {
720            results.put("message", "not-allowed");
721        }
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 (rights = Callable.CHECKED_BY_IMPLEMENTATION)
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 (rights = Callable.NO_CHECK_REQUIRED)
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 (rights = Callable.CHECKED_BY_IMPLEMENTATION)
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}