001/*
002 *  Copyright 2021 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.forms.dao;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023import java.util.function.Function;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import javax.jcr.Node;
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034
035import org.ametys.core.observation.ObservationManager;
036import org.ametys.core.right.RightManager;
037import org.ametys.core.right.RightManager.RightResult;
038import org.ametys.core.ui.Callable;
039import org.ametys.core.user.CurrentUserProvider;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.core.util.LambdaUtils;
042import org.ametys.plugins.forms.FormXpathUtils;
043import org.ametys.plugins.forms.repository.Form;
044import org.ametys.plugins.forms.repository.FormDirectory;
045import org.ametys.plugins.forms.repository.FormDirectoryFactory;
046import org.ametys.plugins.repository.AmetysObject;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.AmetysRepositoryException;
050import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
051import org.ametys.plugins.repository.MovableAmetysObject;
052import org.ametys.plugins.repository.UnknownAmetysObjectException;
053import org.ametys.plugins.repository.jcr.NameHelper;
054import org.ametys.runtime.plugin.component.AbstractLogEnabled;
055import org.ametys.web.repository.site.SiteManager;
056
057import com.google.common.base.Predicates;
058import com.google.common.collect.ImmutableMap;
059
060/**
061 * DAO for manipulating form directories
062 */
063public class FormDirectoryDAO extends AbstractLogEnabled implements Serviceable, Component
064{
065    /** The Avalon role */
066    public static final String ROLE = FormDirectoryDAO.class.getName();
067    
068    /** The alias id of the root {@link FormDirectory} */
069    public static final String ROOT_FORM_DIRECTORY_ID = "root";
070
071    /** The right id to handle forms */
072    public static final String HANDLE_FORM_DIRECTORIES_RIGHT_ID = "FormsDirectory_Rights_Directories";
073    
074    private static final String __ROOT_NODE_NAME = "ametys:forms";
075    
076    private static final String __PLUGIN_NODE_NAME = "forms";
077
078    private static final String __FORMDIRECTORY_NAME_PREFIX = "formdirectory-";
079    
080    /** Observer manager. */
081    protected ObservationManager _observationManager;
082    
083    /** The site manager */
084    protected SiteManager _siteManager;
085    
086    /** The current user provider */
087    protected CurrentUserProvider _userProvider;
088    
089    /** The Ametys object resolver */
090    protected AmetysObjectResolver _resolver;
091    
092    /** The right manager */
093    protected RightManager _rightManager;
094    
095    /** The form DAO */
096    protected FormDAO _formDAO;
097    
098    public void service(ServiceManager manager) throws ServiceException
099    {
100        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
101        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
102        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
103        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
104        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
105        _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE);
106    }
107    
108    /**
109     * Check if a user have write rights on a form directory
110     * @param userIdentity the user
111     * @param formDirectory the form directory
112     * @return true if the user have write rights on a form directory
113     */
114    public boolean hasWriteRightOnFormDirectory(UserIdentity userIdentity, FormDirectory formDirectory)
115    {      
116        return _rightManager.hasRight(userIdentity, HANDLE_FORM_DIRECTORIES_RIGHT_ID, formDirectory) == RightResult.RIGHT_ALLOW;
117    }
118    
119    /**
120     * Check rights for a form element as ametys object
121     * @param formElement the form element as ametys object
122     */
123    public void checkHandleFormDirectoriesRight(AmetysObject formElement)
124    {
125        UserIdentity user = _userProvider.getUser();
126        if (!hasWriteRightOnFormDirectory(user, null))
127        {
128            throw new IllegalAccessError("User '" + user + "' tried to handle form directories without convenient right [" + HANDLE_FORM_DIRECTORIES_RIGHT_ID + "]");
129        }
130    }
131    
132    /**
133     * Get the root plugin storage object.
134     * @param siteName the site's name
135     * @return the root plugin storage object.
136     * @throws AmetysRepositoryException if a repository error occurs.
137     */
138    public FormDirectory getFormDirectoriesRootNode(String siteName) throws AmetysRepositoryException
139    {
140        try
141        {
142            return _getOrCreateRootNode(siteName);
143        }
144        catch (AmetysRepositoryException e)
145        {
146            throw new AmetysRepositoryException("Unable to get the forms root node", e);
147        }
148    }
149    
150    private FormDirectory _getOrCreateRootNode(String siteName) throws AmetysRepositoryException
151    {
152        ModifiableTraversableAmetysObject pluginsNode = _siteManager.getSite(siteName).getRootPlugins();
153        
154        ModifiableTraversableAmetysObject pluginNode = (ModifiableTraversableAmetysObject) _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured");
155        
156        return (FormDirectory) _getOrCreateNode(pluginNode, __ROOT_NODE_NAME, FormDirectoryFactory.FORM_DIRECTORY_NODETYPE);
157    }
158    
159    private AmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
160    {
161        AmetysObject definitionsNode;
162        if (parentNode.hasChild(nodeName))
163        {
164            definitionsNode = parentNode.getChild(nodeName);
165        }
166        else
167        {
168            definitionsNode = parentNode.createChild(nodeName, nodeType);
169            parentNode.saveChanges();
170        }
171        return definitionsNode;
172    }
173    
174    /**
175     * Get the root directory properties
176     * @param siteName the site's name
177     * @return The root directory properties
178     */
179    @Callable
180    public Map<String, Object> getRootProperties(String siteName)
181    {
182        return getFormDirectoryProperties(getFormDirectoriesRootNode(siteName), false);
183    }
184    
185    /**
186     * Get the form directory properties
187     * @param siteName the site's name
188     * @param id The form directory id. Can be {@link #ROOT_FORM_DIRECTORY_ID} for the root directory.
189     * @return The form directory properties
190     */
191    @Callable
192    public Map<String, Object> getFormDirectoryProperties(String siteName, String id)
193    {
194        return getFormDirectoryProperties(getFormDirectory(siteName, id), true);
195    }
196    
197    /**
198     * Get the form directory properties
199     * @param formDirectory The form directory
200     * @param withRights <code>true</code> to add rights to the properties
201     * @return The form directory properties
202     */
203    public Map<String, Object> getFormDirectoryProperties(FormDirectory formDirectory, boolean withRights)
204    {
205        Map<String, Object> infos = new HashMap<>();
206        
207        infos.put("isForm", false);
208        infos.put("id", formDirectory.getId());
209        infos.put("title", formDirectory.getTitle());
210        infos.put("fullPath", getFormDirectoryPath(formDirectory, " > "));
211        
212        FormDirectory formDirectoriesRoot = getFormDirectoriesRootNode(formDirectory.getSiteName());
213        String parentId = formDirectory.getParent().getId().equals(formDirectoriesRoot.getId()) ? ROOT_FORM_DIRECTORY_ID : formDirectory.getParent().getId();
214        infos.put("parentId", parentId);
215        
216        infos.put("hasChildren", formDirectory.getChildren().getSize() != 0);
217
218        UserIdentity currentUser = _userProvider.getUser();
219        boolean canWriteParent = formDirectory.getParent() instanceof FormDirectory && hasWriteRightOnFormDirectory(currentUser, (FormDirectory) formDirectory.getParent());
220        
221        // Used to check if a user have the right to limit access to this directory
222        infos.put("canEditRight", hasRightAffectationRightOnFormDirectory(currentUser, formDirectory));
223        
224        if (withRights)
225        {
226            infos.put("canWriteParent", canWriteParent);
227            infos.put("rights", _getUserRights(formDirectory));
228        }
229        else
230        {
231            boolean canRead = this.hasReadRightOnFormDirectory(currentUser, formDirectory);
232            boolean canWrite = this.hasWriteRightOnFormDirectory(currentUser, formDirectory);
233
234            // Used to check if a user have the right to rename this directory
235            infos.put("canRename", canWrite && canWriteParent);
236            
237            // Used to check if a user have the right to add directories or forms inside this directory
238            infos.put("canWrite", canWrite);
239            
240            // Used to check if a user have the right to delete and drag&drop this directory
241            infos.put("canEdit", canWrite && _hasWriteRightOnEachDescendant(currentUser, formDirectory) && canWriteParent);
242            
243            // Used to filter directories
244            infos.put("displayForRead", canRead || canWrite || hasAnyReadableDescendant(currentUser, formDirectory) || hasAnyWritableDescendant(currentUser, formDirectory));
245            infos.put("displayForWrite", canWrite || _hasAnyWriteDescendantFolder(currentUser, formDirectory));
246            infos.put("displayForRights", canWrite || hasAnyAssignableDescendant(currentUser, formDirectory));
247        }
248        
249        return infos;
250    }
251    
252    /**
253     * Get user rights for the given form directory
254     * @param formDirectory the form directory
255     * @return the set of rights
256     */
257    protected Set<String> _getUserRights (FormDirectory formDirectory)
258    {
259        UserIdentity user = _userProvider.getUser();
260        return _rightManager.getUserRights(user, formDirectory);
261    }
262    
263   /**
264    * Get form directory with given id
265    * @param siteName the site name where to search in jcr
266    * @param id the directory id
267    * @return the form directory having given id
268    */
269    public FormDirectory getFormDirectory(String siteName, String id)
270    {
271        return ROOT_FORM_DIRECTORY_ID.equals(id) 
272                ? getFormDirectoriesRootNode(siteName)
273                : _resolver.resolveById(id);
274    }
275    
276    /**
277     * Get the path of a form directory
278     * @param formDirectory the form directory
279     * @param separator the seperator to use from path
280     * @return the path in form's directory
281     */
282    public String getFormDirectoryPath(FormDirectory formDirectory, String separator)
283    {
284        List<String> fullPath = new ArrayList<>();
285        fullPath.add(formDirectory.getTitle());
286
287        AmetysObject parent = formDirectory.getParent();
288        while (parent instanceof FormDirectory parentDirectory && !isRoot(parentDirectory)) 
289        {
290            fullPath.add(0, parentDirectory.getTitle()); 
291            parent = parent.getParent();
292        }
293        return String.join(separator, fullPath);
294    }
295    
296    /**
297     * Determines if the form directory is the root directory
298     * @param formDirectory the form directory. Cannot be null.
299     * @return true if is the root directory
300     */
301    public boolean isRoot(FormDirectory formDirectory)
302    {
303        return __ROOT_NODE_NAME.equals(formDirectory.getName());
304    }
305    
306    /**
307     * Creates a new {@link FormDirectory}
308     * @param siteName the site's name
309     * @param parentId The id of the parent. Use {@link #ROOT_FORM_DIRECTORY_ID} for the root directory.
310     * @param name The desired name for the new {@link FormDirectory}
311     * @return A result map
312     */
313    @Callable
314    public Map<String, Object> createFormDirectory(String siteName, String parentId, String name)
315    {
316        Map<String, Object> results = new HashMap<>();
317        
318        FormDirectory parent = getFormDirectory(siteName, parentId);
319        checkHandleFormDirectoriesRight(parent);
320        
321        // Find unique and legal name for node
322        String uniqueName = _findUniqueName(parent, name);
323        
324        FormDirectory formDirectory = parent.createChild(uniqueName, FormDirectoryFactory.FORM_DIRECTORY_NODETYPE);
325        formDirectory.setTitle(name);
326        parent.saveChanges();
327        
328        results.put("id", formDirectory.getId());
329        results.put("title", formDirectory.getTitle());
330        results.put("parentId", parent.getId());
331        
332        return results;
333    }
334    
335    private String _findUniqueName(FormDirectory parent, String name)
336    {
337        String legalName;
338        try
339        {
340            legalName = NameHelper.filterName(name);
341        }
342        catch (IllegalArgumentException e)
343        {
344            legalName = NameHelper.filterName(__FORMDIRECTORY_NAME_PREFIX + name);
345        }
346        
347        // Find unique name from legal name
348        String uniqueName = legalName;
349        int index = 2;
350        while (parent.hasChild(uniqueName))
351        {
352            uniqueName = legalName + "-" + (index++);
353        }
354        
355        return uniqueName;
356    }
357    
358    /**
359     * Renames a {@link FormDirectory}
360     * @param id The id of the form directory
361     * @param newName The new name of the directory
362     * @return A result map
363     */
364    @Callable
365    public Map<String, Object> renameFormDirectory(String id, String newName)
366    {
367        FormDirectory directory = _resolver.resolveById(id);
368        checkHandleFormDirectoriesRight(directory);
369        
370        Map<String, Object> results = new HashMap<>();
371        
372        AmetysObject parent = directory.getParent();
373        
374        if (parent instanceof FormDirectory parentDirectory)
375        {
376            UserIdentity currentUser = _userProvider.getUser();
377            
378            boolean canWrite = hasWriteRightOnFormDirectory(currentUser, directory);
379            boolean canWriteParent = hasWriteRightOnFormDirectory(currentUser, parentDirectory);
380            
381            if (canWrite && canWriteParent)
382            {
383                // Find unique and legal name for node
384                String uniqueName = _findUniqueName(parentDirectory, newName);
385                
386                Node node = directory.getNode();
387                try
388                {
389                    node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + uniqueName);
390                    directory.setTitle(newName);
391                    node.getSession().save();
392                    
393                    results.put("id", id);
394                    results.put("title", directory.getTitle());
395                }
396                catch (RepositoryException e)
397                {
398                    getLogger().warn("Form directory renaming failed.", e);
399                    results.put("message", "cannot-rename");
400                }
401            }
402            else
403            {
404                results.put("message", "not-allowed");
405            }
406        }
407        else
408        {
409            results.put("message", "not-allowed");
410        }
411        
412        return results;
413    }
414    
415    /**
416     * Deletes {@link FormDirectory}(s)
417     * @param ids The ids of the form directories to delete
418     * @return A result map
419     */
420    @Callable
421    public Map<String, Object> deleteFormDirectory(List<String> ids)
422    {
423        Map<String, Object> results = new HashMap<>();
424        
425        List<Map<String, Object>> allDeleted = new ArrayList<>();
426        List<Map<String, Object>> allUnknown = new ArrayList<>();
427
428        if (!canDeleteAllFormDirectories(ids))
429        {
430            results.put("message", "not-allowed");
431            return results;
432        }
433        
434        for (String id : ids)
435        {
436            try
437            {
438                FormDirectory directory = _resolver.resolveById(id);
439                String name = directory.getName();
440                String title = directory.getTitle();
441                directory.remove();
442                directory.saveChanges();
443                Map<String, Object> deleted = ImmutableMap.of("id", id, "name", name, "title", title);
444                allDeleted.add(deleted);
445            }
446            catch (UnknownAmetysObjectException e)
447            {
448                Map<String, Object> unknown = ImmutableMap.of("id", id);
449                allUnknown.add(unknown);
450                getLogger().error("Unable to delete form directory. The directory of id '" + id + " doesn't exist", e);
451            }
452        }
453        
454        results.put("deleted", allDeleted);
455        results.put("unknown", allUnknown);
456        
457        return results;
458    }
459    
460    /**
461     * Moves a {@link FormDirectory}
462     * @param siteName name of the site
463     * @param id The id of the form directory
464     * @param newParentId The id of the new parent directory of the form directory. Use {@link #ROOT_FORM_DIRECTORY_ID} for the root directory.
465     * @return A result map
466     */
467    @Callable 
468    public Map<String, Object> moveFormDirectory(String siteName, String id, String newParentId)
469    {
470        Map<String, Object> results = new HashMap<>();
471        FormDirectory formDirectory = _resolver.resolveById(id);
472        FormDirectory parentFormDirectory = getFormDirectory(siteName, newParentId);
473        UserIdentity currentUser = _userProvider.getUser();
474        
475        if (_hasWriteRightOnEachDescendant(currentUser, formDirectory) 
476                && hasWriteRightOnFormDirectory(currentUser, parentFormDirectory))
477        {
478            move(formDirectory, siteName, newParentId, results);
479        }
480        else
481        {
482            results.put("message", "not-allowed");
483        }
484        results.put("id", formDirectory.getId());
485        results.put("title", formDirectory.getTitle());
486        return results;
487    }
488    
489    /**
490     * Moves a {@link FormDirectory} or a {@link Form}
491     * @param obj form or directory to move
492     * @param siteName name of the site
493     * @param newParentId id of directory where to drop obj
494     * @param results a result map
495     */
496    public void move(MovableAmetysObject obj, String siteName, String newParentId, Map<String, Object> results)
497    {
498        FormDirectory newParent = getFormDirectory(siteName, newParentId);
499        if (obj.canMoveTo(newParent))
500        {
501            try
502            {
503                obj.moveTo(newParent, false);
504            }
505            catch (AmetysRepositoryException e)
506            {
507                getLogger().warn("Form moving failed.", e);
508                results.put("message", "cannot-move");
509            }
510        }
511        else
512        {
513            results.put("message", "cannot-move");
514        }
515    }
516    
517    /**
518     * Determines if application must warn before deleting the given {@link FormDirectory}s
519     * @param ids The {@link FormDirectory} ids
520     * @return <code>true</code> if application must warn
521     */
522    @Callable
523    public boolean mustWarnBeforeDeletion(List<String> ids)
524    {
525        return ids.stream()
526                .anyMatch(LambdaUtils.wrapPredicate(this::_mustWarnBeforeDeletion));
527    }
528    
529    private boolean _mustWarnBeforeDeletion(String id)
530    {
531        FormDirectory directory = _resolver.resolveById(id);
532        AmetysObjectIterable<Form> allForms = getChildFormsForAdministrator(directory, false);
533        return _containsNotOwnForms(allForms);
534    }
535    
536    private boolean _containsNotOwnForms(AmetysObjectIterable<Form> allForms)
537    {
538        UserIdentity currentUser = _userProvider.getUser();
539        return allForms.stream()
540                .map(Form::getAuthor)
541                .anyMatch(Predicates.not(currentUser::equals));
542    }
543    
544    /**
545     * Can the current user delete all the {@link FormDirectory}s from the list ?
546     * @param ids The {@link FormDirectory} ids
547     * @return <code>true</code> if he can
548     */
549    protected boolean canDeleteAllFormDirectories(List<String> ids)
550    {
551        return canDeleteFormDirectories(ids).values()
552                .stream()
553                .allMatch(Boolean.TRUE::equals);
554    }
555    
556    /**
557     * Determines if the current user can delete the given {@link FormDirectory}s
558     * @param ids The {@link FormDirectory} ids
559     * @return A map with <code>true</code> for each id if the current user can delete the associated {@link FormDirectory}
560     */
561    @Callable
562    public Map<String, Boolean> canDeleteFormDirectories(List<String> ids)
563    {
564        return ids.stream()
565                .collect(Collectors.toMap(
566                        Function.identity(), 
567                        LambdaUtils.wrap(this::_canDeleteFormDirectory)));
568    }
569    
570    private boolean _canDeleteFormDirectory(String id)
571    {
572        if (ROOT_FORM_DIRECTORY_ID.equals(id))
573        {
574            return false;
575        }
576        else
577        {
578            UserIdentity user = _userProvider.getUser();
579            FormDirectory directory = _resolver.resolveById(id);
580            
581            return _hasWriteRightOnEachDescendant(user, directory) 
582                        && !_hasPublishedForms(directory)
583                        && directory.getParent() instanceof FormDirectory 
584                        && hasWriteRightOnFormDirectory(user, (FormDirectory) directory.getParent());
585        }
586    }
587    
588    private boolean _hasPublishedForms(FormDirectory formDirectory)
589    {
590        boolean hasPublishedForms = false;
591        for (AmetysObject child : formDirectory.getChildren())
592        {
593            if (child instanceof FormDirectory)
594            {
595                hasPublishedForms = hasPublishedForms || _hasPublishedForms((FormDirectory) child);
596            }
597            else if (child instanceof Form)
598            {
599                hasPublishedForms = hasPublishedForms || !_formDAO.getFormPage(child.getId(), formDirectory.getSiteName()).isEmpty();
600            }
601        }
602        
603        return hasPublishedForms;
604    }
605    
606    /**
607     * Check if a user have write rights on an form directory and each of his descendant
608     * @param userIdentity the user
609     * @param formDirectory the form directory
610     * @return true if the user have write rights on an form directory and each of his descendant
611     */
612    private boolean _hasWriteRightOnEachDescendant(UserIdentity userIdentity, FormDirectory formDirectory)
613    {
614        boolean hasRight = hasWriteRightOnFormDirectory(userIdentity, formDirectory);
615        if (!hasRight)
616        {
617            return false;
618        }
619        
620        try (AmetysObjectIterable<AmetysObject> children = formDirectory.getChildren())
621        {
622            for (AmetysObject child : children)
623            {
624                if (child instanceof FormDirectory)
625                {
626                    hasRight = hasRight && _hasWriteRightOnEachDescendant(userIdentity, (FormDirectory) child);
627                }
628                else if (child instanceof Form)
629                {
630                    hasRight = hasRight && hasWriteRightOnFormAndParent(userIdentity, (Form) child, formDirectory);
631                }
632                
633                if (!hasRight)
634                {
635                    return false;
636                }
637            }
638            
639            return hasRight;
640        }
641    }
642    
643    /**
644     * Check if a user have write rights on a form
645     * @param userIdentity the user
646     * @param form the source for the form
647     * @param formDirectory the form directory
648     * @return true if the user have write rights on a form
649     */
650    public boolean hasWriteRightOnFormAndParent(UserIdentity userIdentity, Form form, FormDirectory formDirectory)
651    {
652        return form != null && userIdentity.equals(form.getAuthor()) || _formDAO.hasWriteRightOnForm(userIdentity, form) && hasWriteRightOnFormDirectory(userIdentity, formDirectory);
653    }
654    
655    /**
656     * Gets all forms in WRITE access for given parent
657     * @param parent The {@link FormDirectory}, defining the context from which getting children
658     * @param onlyDirect <code>true</code> in order to have only direct child forms from parent path, <code>false</code> otherwise to have all forms at any level underneath the parent path
659     * @param user The user
660     * @return all forms in WRITE access for given parent
661     */
662    public Stream<Form> getChildFormsInWriteAccess(FormDirectory parent, boolean onlyDirect, UserIdentity user)
663    {
664        return _resolver.query(FormXpathUtils.getXPathForForms(parent, onlyDirect))
665                .stream()
666                .filter(Form.class::isInstance)
667                .map(obj -> (Form) obj)
668                .filter(form -> _formDAO.hasWriteRightOnForm(user, form));
669    }
670    
671    /**
672     * Gets all forms in WRITE access for given parent
673     * @param parent The {@link FormDirectory}, defining the context from which getting children
674     * @param onlyDirect <code>true</code> in order to have only direct child forms from parent path, <code>false</code> otherwise to have all forms at any level underneath the parent path
675     * @param user The user
676     * @return all forms in WRITE access for given parent
677     */
678    public Stream<Form> getChildFormsInRightAccess(FormDirectory parent, boolean onlyDirect, UserIdentity user)
679    {
680        return _resolver.query(FormXpathUtils.getXPathForForms(parent, onlyDirect))
681                .stream()
682                .filter(Form.class::isInstance)
683                .map(obj -> (Form) obj)
684                .filter(form -> _formDAO.hasRightAffectationRightOnForm(user, form));
685    }
686    
687    /**
688     * Gets all forms in READ access for given parent
689     * @param parent The {@link FormDirectory}, defining the context from which getting children
690     * @param onlyDirect <code>true</code> in order to have only direct child forms from parent path, <code>false</code> otherwise to have all forms at any level underneath the parent path
691     * @param user The user
692     * @return all forms in READ access for given parent
693     */
694    public Stream<Form> getChildFormsInReadAccess(FormDirectory parent, boolean onlyDirect, UserIdentity user)
695    {
696        return _resolver.query(FormXpathUtils.getXPathForForms(parent, onlyDirect))
697                .stream()
698                .filter(Form.class::isInstance)
699                .map(obj -> (Form) obj)
700                        .filter(form -> _formDAO.hasReadRightOnForm(user, form));
701    }
702
703    /**
704     * Gets all forms for administrator for given parent
705     * @param parent The {@link FormDirectory}, defining the context from which getting children
706     * @param onlyDirect <code>true</code> in order to have only direct child forms from parent path, <code>false</code> otherwise to have all forms at any level underneath the parent path
707     * @return all forms for administrator for given parent
708     */
709    public AmetysObjectIterable<Form> getChildFormsForAdministrator(FormDirectory parent, boolean onlyDirect)
710    {
711        return _resolver.query(FormXpathUtils.getXPathForForms(parent, onlyDirect));
712    }
713    
714    /**
715     * Gets all form directories for given parent
716     * @param parent The {@link FormDirectory}, defining the context from which getting children
717     * @return all form directories for given parent
718     */
719    public AmetysObjectIterable<FormDirectory> getChildFormDirectories(FormDirectory parent)
720    {
721        return _resolver.query(FormXpathUtils.getXPathForFormDirectories(parent));
722    }
723    
724    /**
725     * Provides the current user.
726     * @return the user which cannot be <code>null</code>.
727     */
728    protected UserIdentity _getCurrentUser()
729    {      
730        return _userProvider.getUser();
731    }
732    
733    /**
734     * Check if a user have write rights on a form directory
735     * @param userIdentity the user
736     * @param formDirectory the form directory
737     * @return true if the user have write rights on a form
738     */
739    public boolean hasRightAffectationRightOnFormDirectory(UserIdentity userIdentity, FormDirectory formDirectory)
740    {
741        return hasWriteRightOnFormDirectory(userIdentity, formDirectory) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW;
742    }
743    
744    
745    /**
746     * Check if a user have read rights on a form directory
747     * @param userIdentity the user
748     * @param directory the form directory
749     * @return true if the user have read rights on a form directory
750     */
751    public boolean hasReadRightOnFormDirectory(UserIdentity userIdentity, FormDirectory directory)
752    {
753        return _rightManager.hasReadAccess(userIdentity, directory);
754    }
755    
756    /**
757     * Check if a directory have a descendant in read access for a given user
758     * @param userIdentity the user
759     * @param formDirectory the form directory
760     * @return true if the user have read right for at least one child of this directory
761     */
762    public Boolean hasAnyReadableDescendant(UserIdentity userIdentity, FormDirectory formDirectory)
763    {
764        boolean hasDescendant = hasReadRightOnFormDirectory(userIdentity, formDirectory);
765        if (hasDescendant)
766        {
767            return true;
768        }
769        
770        try (AmetysObjectIterable<AmetysObject> children = formDirectory.getChildren())
771        {
772            for (AmetysObject child : children)
773            {
774                if (child instanceof FormDirectory)
775                {
776                    hasDescendant = hasDescendant || hasAnyReadableDescendant(userIdentity, (FormDirectory) child);
777                }
778                else if (child instanceof Form)
779                {
780                    hasDescendant = hasDescendant || _formDAO.hasReadRightOnForm(userIdentity, (Form) child);
781                }
782                
783                if (hasDescendant)
784                {
785                    return true;
786                }
787            }
788            
789            return hasDescendant;
790        }
791    }
792
793    /**
794     * Check if a directory have descendant in write access for a given user
795     * @param userIdentity the user
796     * @param formDirectory the form directory
797     * @return true if the user have write right for at least one child of this directory
798     */
799    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, FormDirectory formDirectory)
800    {
801        boolean hasDescendant = hasWriteRightOnFormDirectory(userIdentity, formDirectory);
802        if (hasDescendant)
803        {
804            return true;
805        }
806        
807        try (AmetysObjectIterable<AmetysObject> children = formDirectory.getChildren())
808        {
809            for (AmetysObject child : children)
810            {
811                if (child instanceof FormDirectory)
812                {
813                    hasDescendant = hasDescendant || hasAnyWritableDescendant(userIdentity, (FormDirectory) child);
814                }
815                else if (child instanceof Form)
816                {
817                    hasDescendant = hasDescendant || hasWriteRightOnFormAndParent(userIdentity, (Form) child, formDirectory);
818                }
819                
820                if (hasDescendant)
821                {
822                    return true;
823                }
824            }
825        }
826
827        return hasDescendant;
828    }
829    
830    private Boolean _hasAnyWriteDescendantFolder(UserIdentity userIdentity, FormDirectory formDirectory)
831    {
832        boolean hasDescendant = hasWriteRightOnFormDirectory(userIdentity, formDirectory);
833        if (hasDescendant)
834        {
835            return true;
836        }
837        
838        try (AmetysObjectIterable<AmetysObject> children = formDirectory.getChildren())
839        {
840            for (AmetysObject child : children)
841            {
842                if (child instanceof FormDirectory)
843                {
844                    hasDescendant = hasDescendant || _hasAnyWriteDescendantFolder(userIdentity, (FormDirectory) child);
845                }
846                
847                if (hasDescendant)
848                {
849                    return true;
850                }
851            }
852            
853            return hasDescendant;
854        }
855    }
856    
857    /**
858     * Check if a directory have descendant in right assignment access for a given user
859     * @param userIdentity the user
860     * @param formDirectory the form directory
861     * @return true if the user have right assignment right for at least one child of this directory
862     */
863    public Boolean hasAnyAssignableDescendant(UserIdentity userIdentity, FormDirectory formDirectory)
864    {
865        boolean hasDescendant = hasRightAffectationRightOnFormDirectory(userIdentity, formDirectory);
866        if (hasDescendant)
867        {
868            return true;
869        }
870        
871        try (AmetysObjectIterable<AmetysObject> children = formDirectory.getChildren())
872        {
873            for (AmetysObject child : children)
874            {
875                if (child instanceof FormDirectory)
876                {
877                    hasDescendant = hasDescendant || hasAnyAssignableDescendant(userIdentity, (FormDirectory) child);
878                }
879                else if (child instanceof Form)
880                {
881                    hasDescendant = hasDescendant || _formDAO.hasRightAffectationRightOnForm(userIdentity, (Form) child);
882                }
883                
884                if (hasDescendant)
885                {
886                    return true;
887                }
888            }
889            
890            return hasDescendant;
891        }
892    }
893}