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