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