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