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