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.stream.Collectors;
024
025import javax.jcr.Node;
026import javax.jcr.RepositoryException;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.jackrabbit.util.Text;
033
034import org.ametys.core.observation.Event;
035import org.ametys.core.observation.ObservationManager;
036import org.ametys.core.right.RightManager;
037import org.ametys.core.ui.Callable;
038import org.ametys.core.user.CurrentUserProvider;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.util.I18nUtils;
041import org.ametys.plugins.forms.FormEvents;
042import org.ametys.plugins.forms.question.types.ChoicesListQuestionType;
043import org.ametys.plugins.forms.repository.Form;
044import org.ametys.plugins.forms.repository.FormPage;
045import org.ametys.plugins.forms.repository.FormPageFactory;
046import org.ametys.plugins.forms.repository.FormPageRule;
047import org.ametys.plugins.forms.repository.FormPageRule.PageRuleType;
048import org.ametys.plugins.forms.repository.FormQuestion;
049import org.ametys.plugins.repository.AmetysObject;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.AmetysRepositoryException;
052import org.ametys.plugins.repository.UnknownAmetysObjectException;
053import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
054import org.ametys.plugins.repository.jcr.JCRAmetysObject;
055import org.ametys.plugins.repository.jcr.NameHelper;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058
059/**
060 * DAO for manipulating form pages
061 */
062public class FormPageDAO extends AbstractLogEnabled implements Serviceable, Component
063{
064    /** The Avalon role */
065    public static final String ROLE = FormPageDAO.class.getName();
066    /** Observer manager. */
067    protected ObservationManager _observationManager;
068    /** The Ametys object resolver */
069    protected AmetysObjectResolver _resolver;
070    /** The current user provider. */
071    protected CurrentUserProvider _currentUserProvider;
072    /** The form question DAO */
073    protected FormQuestionDAO _formQuestionDAO;
074    /** I18n Utils */
075    protected I18nUtils _i18nUtils;
076    /** The form DAO */
077    protected FormDAO _formDAO;
078    /** The right manager */
079    protected RightManager _rightManager;
080    
081    public void service(ServiceManager manager) throws ServiceException
082    {
083        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
084        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
085        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
086        _formQuestionDAO = (FormQuestionDAO) manager.lookup(FormQuestionDAO.ROLE);
087        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
088        _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE);
089        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
090    }
091
092    /**
093     * Creates a {@link FormPage}.
094     * @param parentId The id of the parent.
095     * @param name The desired name for the new {@link FormPage}
096     * @return The id of the created form page
097     * @throws Exception if an error occurs during the form creation process
098     */
099    @Callable
100    public Map<String, String> createPage (String parentId, String name) throws Exception
101    {
102        Map<String, String> result = new HashMap<>();
103        
104        Form rootNode = _resolver.resolveById(parentId);
105        _formDAO.checkHandleFormRight(rootNode);
106        
107        // Find unique name
108        String originalName = NameHelper.filterName(name);
109        String uniqueName = originalName;
110        int index = 1;
111        while (rootNode.hasChild(uniqueName))
112        {
113            uniqueName = originalName + "-" + index;
114            index++;
115        }
116        
117        FormPage page = rootNode.createChild(uniqueName, FormPageFactory.FORM_PAGE_NODETYPE);
118        
119        page.setTitle(name + " " + index);
120        
121        rootNode.saveChanges();
122
123        Map<String, Object> eventParams = new HashMap<>();
124        eventParams.put("form", rootNode);
125        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
126        
127        
128        result.put("id", page.getId());
129        result.put("name", page.getTitle());
130        
131        return result;
132    }
133    
134    /**
135     * Rename a {@link FormPage}
136     * @param id The id of the page 
137     * @param newName The new name of the page
138     * @return A result map
139     */
140    @Callable
141    public Map<String, String> renamePage (String id, String newName)
142    {
143        Map<String, String> results = new HashMap<>();
144        
145        FormPage page = _resolver.resolveById(id);
146        _formDAO.checkHandleFormRight(page);
147        
148        String legalName = Text.escapeIllegalJcrChars(newName);
149        Node node = page.getNode();
150        try
151        {
152            page.setTitle(newName);
153            
154            node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName);
155            node.getSession().save();
156            
157            Map<String, Object> eventParams = new HashMap<>();
158            eventParams.put("form", page.getForm());
159            _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
160            
161            results.put("id", id);
162            results.put("newName", legalName);
163            results.put("formId", page.getForm().getId());
164        }
165        catch (RepositoryException e)
166        {
167            getLogger().error("Form renaming failed.", e);
168            results.put("message", "cannot-rename");
169        }
170        
171        return results;
172    }
173    
174    /**
175     * Deletes a {@link FormPage}.
176     * @param id The id of the page to delete
177     * @return The id of the page
178     */
179    @Callable
180    public Map<String, String> deletePage (String id)
181    {
182        Map<String, String> result = new HashMap<>();
183        
184        FormPage page = _resolver.resolveById(id);
185        _formDAO.checkHandleFormRight(page);
186        
187        Form parent = page.getForm();
188        
189        //remove question rules references
190        _removeReferencesFromQuestionsRules(page, parent);
191        
192        page.remove();
193        
194        // Remove page rules references
195        _removeReferencesFromPages (id, parent);
196        _removeReferencesFromQuestions(id, parent);
197        parent.saveChanges();
198        
199        Map<String, Object> eventParams = new HashMap<>();
200        eventParams.put("form", parent);
201        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
202        
203        result.put("id", id);
204        result.put("formId", parent.getId());
205        
206        return result;
207    }
208    
209    /**
210     * Copies and pastes a form page.
211     * @param formId The id of the form, target of the copy
212     * @param pageId The id of the page to copy
213     * @return The id of the created page
214     */
215    @Callable
216    public Map<String, String> copyPage(String formId, String pageId)
217    {
218        Map<String, String> result = new HashMap<>();
219        
220        Form parentForm = _resolver.resolveById(formId);
221        _formDAO.checkHandleFormRight(parentForm);
222        
223        FormPage originalPage = _resolver.resolveById(pageId);
224        
225        // Find unique name
226        String originalName = originalPage.getName();
227        String name = originalName;
228        int index = 2;
229        while (parentForm.hasChild(name))
230        {
231            name = originalName + "-" + (index++);
232        }
233        FormPage cPage = originalPage.copyTo(parentForm, name);
234        String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalPage.getTitle();
235        cPage.setTitle(copyTitle);
236        cPage.deleteRule();
237        
238        parentForm.saveChanges();
239        
240        Map<String, Object> eventParams = new HashMap<>();
241        eventParams.put("form", parentForm);
242        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
243        
244        result.put("id", cPage.getId());
245        result.put("formId", parentForm.getId());
246        
247        return result;
248    }
249    
250    /**
251     * Moves an element of the form.
252     * @param id The id of the element to move.
253     * @param oldParent The id of the element's parent.
254     * @param newParent The id of the new element's parent.
255     * @param index The index where to move. null to place the element at the end.
256     * @return A map with the ids of the element, the old parent and the new parent
257     * @throws Exception if an error occurs when moving an element of the form
258     */
259    @Callable
260    public Map<String, String> moveObject (String id, String oldParent, String newParent, long index) throws Exception
261    {
262        Map<String, String> result = new HashMap<>();
263        
264        JCRAmetysObject aoMoved = _resolver.resolveById(id);
265        _formDAO.checkHandleFormRight(aoMoved);
266        
267        DefaultTraversableAmetysObject newParentAO = _resolver.resolveById(newParent);
268        JCRAmetysObject  brother = null;
269        long size = newParentAO.getChildren().getSize();
270        if (index != -1 && index < size)
271        {
272            brother = newParentAO.getChildAt(index);
273        }
274        else if (index >= size)
275        {
276            brother = newParentAO.getChildAt(Math.toIntExact(size) - 1);
277        }
278        Form oldForm = getParentForm(aoMoved);
279        if (oldForm != null)
280        {
281            result.put("oldFormId", oldForm.getId());
282        }
283        
284        if (oldParent.equals(newParent) && brother != null)
285        {
286            Node node = aoMoved.getNode();
287            String name = "";
288            try
289            {
290                name = (index == size)
291                            ? null 
292                            : brother.getName();
293                node.getParent().orderBefore(node.getName(), name);
294            }
295            catch (RepositoryException e)
296            {
297                throw new AmetysRepositoryException(String.format("Unable to order AmetysOject '%s' before sibling '%s'", this, name), e);
298            }
299        }
300        else
301        {
302            Node node = aoMoved.getNode();
303            
304            String name = node.getName();
305            // Find unused name on new parent node
306            int localIndex = 2;
307            while (newParentAO.hasChild(name))
308            {
309                name = node.getName() + "-" + localIndex++;
310            }
311            
312            node.getSession().move(node.getPath(), newParentAO.getNode().getPath() + "/" + name);
313            
314            if (brother != null)
315            {
316                node.getParent().orderBefore(node.getName(), brother.getName());
317            }
318        }
319        
320        if (newParentAO.needsSave())
321        {
322            newParentAO.saveChanges();
323        }
324        
325        Form form = getParentForm(aoMoved);
326        if (form != null)
327        {
328            result.put("newFormId", form.getId());
329            
330            Map<String, Object> eventParams = new HashMap<>();
331            eventParams.put("form", form);
332            _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
333        }
334        
335        result.put("id", id);
336        
337        if (aoMoved instanceof FormPage)
338        {
339            result.put("type", "page");
340        }
341        else if (aoMoved instanceof FormQuestion)
342        {
343            result.put("type", "question");
344            result.put("questionType", ((FormQuestion) aoMoved).getType().getId());
345        }
346        
347        result.put("newParentId", newParentAO.getId());
348        result.put("oldParentId", oldParent);
349        
350        return result;
351    }
352    
353    /**
354     * Get the page's properties
355     * @param pageId The form page's id
356     * @return The page properties
357     */
358    @Callable 
359    public Map<String, Object> getPageProperties (String pageId)
360    {
361        try
362        {
363            FormPage page = _resolver.resolveById(pageId);
364            return getPageProperties(page, true);
365        }
366        catch (UnknownAmetysObjectException e)
367        {
368            getLogger().warn("Can't find page with id: {}. It probably has just been deleted", pageId, e);
369            Map<String, Object> infos = new HashMap<>();
370            infos.put("id", pageId);
371            return infos;
372        }
373    }
374    
375    /**
376     * Get the page's properties
377     * @param page The form page
378     * @param withRights <code>true</code> to have the rights in the properties
379     * @return The page properties
380     */
381    public Map<String, Object> getPageProperties (FormPage page, boolean withRights)
382    {
383        Map<String, Object> infos = new HashMap<>();
384        
385        List<String> fullPath = new ArrayList<>();
386        fullPath.add(page.getTitle());
387
388        AmetysObject node = page.getParent();
389        fullPath.add(0, node.getName()); 
390        
391        infos.put("type", "page");
392        
393        /** Use in the bus message */
394        infos.put("id", page.getId());
395        infos.put("title", page.getTitle());
396        infos.put("formId", page.getForm().getId());
397        infos.put("hasEntries", !page.getForm().getEntries().isEmpty());
398        infos.put("hasChildren", page.getQuestions().size() > 0);
399        
400        boolean isConfigured = !page.getQuestions().stream().anyMatch(q -> !q.getType().isQuestionConfigured(q));
401        infos.put("isConfigured", isConfigured);
402        
403        if (withRights)
404        {
405            infos.put("rights", _getUserRights(page));
406        }
407        else
408        {
409            infos.put("canWrite", _formDAO.hasWriteRightOnForm(_currentUserProvider.getUser(), page));
410        }
411
412        return infos;
413    }
414    
415    /**
416     * Get user rights for the given form page
417     * @param page the form page
418     * @return the set of rights
419     */
420    protected Set<String> _getUserRights (FormPage page)
421    {
422        UserIdentity user = _currentUserProvider.getUser();
423        return _rightManager.getUserRights(user, page);
424    }
425    
426    /**
427     * Gets the ids of the path elements of a form component, i.e. the parent ids.
428     * <br>For instance, if the page path is 'a/b/c', then the result list will be ["id-of-a", "id-of-b", "id-of-c"]
429     * @param id The id of the form component
430     * @return the ids of the path elements of a form
431     */
432    @Callable
433    public List<String> getIdsOfPath(String id)
434    {
435        AmetysObject formComponent = _resolver.resolveById(id);
436        
437        if (!(formComponent instanceof FormPage) && !(formComponent instanceof FormQuestion))
438        {
439            throw new IllegalArgumentException("The given id is not a form component");
440        }
441        
442        List<String> pathElements = new ArrayList<>();
443        AmetysObject current = formComponent.getParent();
444        while (!(current instanceof Form))
445        {
446            pathElements.add(0, current.getId());
447            current = current.getParent();
448        }
449        
450        return pathElements;
451    }
452    
453    /**
454     * Gets all pages for given parent
455     * @param formId The id of the {@link Form}, defining the context from which getting children
456     * @return all forms  for given parent
457     */
458    @Callable
459    public List<Map<String, Object>> getChildPages(String formId)
460    {
461        Form form = _resolver.resolveById(formId);
462        _formDAO.checkHandleFormRight(form);
463        
464        return form.getPages()
465                    .stream()
466                    .map(p -> this.getPageProperties(p, false))
467                    .toList();
468    }
469    
470    /**
471     * Get the form containing the given object.
472     * @param obj the object.
473     * @return the parent Form.
474     */
475    protected Form getParentForm(JCRAmetysObject obj)
476    {
477        try
478        {
479            JCRAmetysObject currentAo = obj.getParent();
480            
481            while (!(currentAo instanceof Form))
482            {
483                currentAo = currentAo.getParent();
484            }
485            
486            if (currentAo instanceof Form)
487            {
488                return (Form) currentAo;
489            }
490        }
491        catch (AmetysRepositoryException e)
492        {
493            // Ignore, just return null.
494        }
495        
496        return null;
497    }
498    
499    /**
500     * Determines if a page is the last of form's pages.
501     * @param id The page id
502     * @return True if the page is the last one. 
503     */
504    @Callable
505    public boolean isLastPage (String id)
506    {
507        FormPage page = _resolver.resolveById(id);
508        _formDAO.checkHandleFormRight(page);
509        
510        Form form = page.getForm();
511        
512        List<FormPage> pages = form.getPages();
513        FormPage lastPage = pages.get(pages.size() - 1);
514        
515        return id.equals(lastPage.getId());
516    }
517    
518    /**
519     * Gets the branches for a form page.
520     * @param pageId The id of the form page.
521     * @return The branches
522     */
523    @Callable
524    public Map<String, Object> getBranches (String pageId)
525    {
526        Map<String, Object> result = new HashMap<>();
527        
528        FormPage page = _resolver.resolveById(pageId);
529        _formDAO.checkHandleFormRight(page);
530        
531        result.put("id", pageId);
532        
533        List<Object> questions = new ArrayList<>();
534        List<FormQuestion> questionsAO = page.getQuestions();
535        int index = 1;
536        for (FormQuestion question : questionsAO)
537        {
538            if (question.getType() instanceof ChoicesListQuestionType type 
539                && !type.getSourceType(question).remoteData()
540                && !question.isModifiable())
541            {
542                try
543                {
544                    questions.add(_formQuestionDAO.getRules(question.getId(), index));
545                }
546                catch (Exception e)
547                {
548                    getLogger().error("an exception occured while getting rules for question " + question.getId());
549                }
550            }
551            index++;
552        }
553        result.put("questions", questions);
554        
555        // SAX page rule
556        result.put("rule", getRule(pageId));
557        
558        return result;
559    }
560    
561    /**
562     * Gets the rule for a form page.
563     * @param id The id of the form page.
564     * @return The rule, or null
565     */
566    @Callable
567    public Map<String, Object> getRule (String id)
568    {
569        Map<String, Object> result = new HashMap<>();
570        
571        FormPage page = _resolver.resolveById(id);
572        _formDAO.checkHandleFormRight(page);
573        
574        FormPageRule rule = page.getRule();
575        
576        if (rule != null)
577        {
578            result.put("type", rule.getType().name());
579            String pageId = rule.getPageId();
580            if (pageId != null)
581            {
582                try
583                {
584                    FormPage pageAO = _resolver.resolveById(pageId);
585                    result.put("page", pageId);
586                    result.put("pageName", pageAO.getTitle());
587                }
588                catch (UnknownAmetysObjectException e)
589                {
590                    // The page does not exist anymore
591                }
592            }
593        }
594        else
595        {
596            result = null;
597        }
598        
599        return result;
600    }
601    
602    /**
603     * Adds a a new rule to a page.
604     * @param id The id of the page
605     * @param rule The rule type
606     * @param page The page to jump or skip
607     * @return An empty map
608     */
609    @Callable
610    public Map<String, Object> addRule (String id, String rule, String page)
611    {
612        FormPage formPage = _resolver.resolveById(id);
613        _formDAO.checkHandleFormRight(formPage);
614        
615        formPage.setRule(PageRuleType.valueOf(rule), page);
616        formPage.saveChanges();
617        
618        Map<String, Object> eventParams = new HashMap<>();
619        eventParams.put("form", formPage.getForm());
620        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
621        
622        return new HashMap<>();
623    }
624
625    /**
626     * Deletes a rule to a page
627     * @param id The id of the page
628     * @return An empty map
629     */
630    @Callable
631    public Map<String, Object> deleteRule (String id)
632    {
633        FormPage formPage = _resolver.resolveById(id);
634        _formDAO.checkHandleFormRight(formPage);
635        
636        formPage.deleteRule();
637        formPage.saveChanges();
638        
639        Map<String, Object> eventParams = new HashMap<>();
640        eventParams.put("form", formPage.getForm());
641        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams));
642        
643        return new HashMap<>();
644    }
645    
646    private void _removeReferencesFromPages (String pageId, Form parent)
647    {
648        List<FormPageRule> rulesWithPageId = parent.getPages().stream()
649            .map(page -> page.getRule())
650            .filter(rule -> rule != null && pageId.equals(rule.getPageId()))
651            .collect(Collectors.toList());
652        
653        for (FormPageRule rule : rulesWithPageId)
654        {
655            rule.remove();
656        }
657        parent.saveChanges();
658    }
659    
660    private void _removeReferencesFromQuestions (String pageId, Form parent)
661    {
662        List<FormPageRule> rulesWithPageId = parent.getQuestions().stream()
663            .map(question -> question.getPageRules())
664            .flatMap(List::stream)
665            .filter(rule -> rule != null && pageId.equals(rule.getPageId()))
666            .collect(Collectors.toList());
667            
668        for (FormPageRule rule : rulesWithPageId)
669        {
670            rule.remove();
671        }
672        parent.saveChanges();
673    }
674    
675    private void _removeReferencesFromQuestionsRules (FormPage page, Form parent)
676    {
677        for (FormQuestion questionToDelete : page.getQuestions())
678        {
679            parent.deleteQuestionsRule(questionToDelete.getId());
680        }
681    }
682    
683    /**
684     * Provides the current user.
685     * @return the user which cannot be <code>null</code>.
686     */
687    protected UserIdentity _getCurrentUser()
688    {      
689        return _currentUserProvider.getUser();
690    }
691}