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.time.LocalDate;
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import javax.jcr.Node;
029import javax.jcr.Repository;
030import javax.jcr.RepositoryException;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.commons.lang.StringUtils;
037
038import org.ametys.core.observation.Event;
039import org.ametys.core.observation.ObservationManager;
040import org.ametys.core.right.RightManager;
041import org.ametys.core.right.RightManager.RightResult;
042import org.ametys.core.ui.Callable;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.core.util.DateUtils;
046import org.ametys.core.util.I18nUtils;
047import org.ametys.plugins.core.user.UserHelper;
048import org.ametys.plugins.forms.FormEvents;
049import org.ametys.plugins.forms.helper.ScheduleOpeningHelper;
050import org.ametys.plugins.forms.repository.CopyFormUpdater;
051import org.ametys.plugins.forms.repository.CopyFormUpdaterExtensionPoint;
052import org.ametys.plugins.forms.repository.Form;
053import org.ametys.plugins.forms.repository.FormDirectory;
054import org.ametys.plugins.forms.repository.FormEntry;
055import org.ametys.plugins.forms.repository.FormFactory;
056import org.ametys.plugins.forms.repository.FormQuestion;
057import org.ametys.plugins.repository.AmetysObject;
058import org.ametys.plugins.repository.AmetysObjectIterable;
059import org.ametys.plugins.repository.AmetysObjectResolver;
060import org.ametys.plugins.repository.ModifiableAmetysObject;
061import org.ametys.plugins.repository.UnknownAmetysObjectException;
062import org.ametys.plugins.repository.jcr.NameHelper;
063import org.ametys.plugins.repository.provider.AbstractRepository;
064import org.ametys.plugins.workflow.support.WorkflowHelper;
065import org.ametys.runtime.i18n.I18nizableText;
066import org.ametys.runtime.model.ElementDefinition;
067import org.ametys.runtime.plugin.component.AbstractLogEnabled;
068import org.ametys.web.parameters.view.ViewParametersManager;
069import org.ametys.web.repository.page.ModifiableZoneItem;
070import org.ametys.web.repository.page.Page;
071import org.ametys.web.repository.page.SitemapElement;
072import org.ametys.web.repository.page.ZoneDAO;
073import org.ametys.web.repository.page.ZoneItem;
074import org.ametys.web.repository.site.SiteManager;
075import org.ametys.web.service.Service;
076import org.ametys.web.service.ServiceExtensionPoint;
077
078/**
079 * The form DAO
080 */
081public class FormDAO extends AbstractLogEnabled implements Serviceable, Component
082{
083    /** The Avalon role */
084    public static final String ROLE = FormDAO.class.getName();
085    /** The right id to handle forms */
086    public static final String HANDLE_FORMS_RIGHT_ID = "Plugins_Forms_Right_Handle";
087
088    private static final String __FORM_NAME_PREFIX = "form-";
089    
090    /** The Ametys object resolver */
091    protected AmetysObjectResolver _resolver;
092    /** The current user provider */
093    protected CurrentUserProvider _userProvider;
094    /** I18n Utils */
095    protected I18nUtils _i18nUtils;
096    /** The form directory DAO */
097    protected FormDirectoryDAO _formDirectoryDAO;
098    /** The form page DAO */
099    protected FormPageDAO _formPageDAO;
100    /** The form entry DAO */
101    protected FormEntryDAO _formEntryDAO;
102    /** The user helper */
103    protected UserHelper _userHelper;
104    /** The JCR repository. */
105    protected Repository _repository;
106    /** The right manager */
107    protected RightManager _rightManager;
108    /** The service extension point */
109    protected ServiceExtensionPoint _serviceEP;
110    /** The zone DAO */
111    protected ZoneDAO _zoneDAO;
112    /** The schedule opening helper */
113    protected ScheduleOpeningHelper _scheduleOpeningHelper;
114    /** The workflow helper */
115    protected WorkflowHelper _workflowHelper;
116    /** The site manager */
117    protected SiteManager _siteManager;
118    /** Observer manager. */
119    protected ObservationManager _observationManager;
120    /** The current user provider. */
121    protected CurrentUserProvider _currentUserProvider;
122    
123    /** The copy form updater extension point */
124    protected CopyFormUpdaterExtensionPoint _copyFormEP;
125    
126    public void service(ServiceManager manager) throws ServiceException
127    {
128        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
129        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
130        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
131        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
132        _formDirectoryDAO = (FormDirectoryDAO) manager.lookup(FormDirectoryDAO.ROLE);
133        _formPageDAO = (FormPageDAO) manager.lookup(FormPageDAO.ROLE);
134        _formEntryDAO = (FormEntryDAO) manager.lookup(FormEntryDAO.ROLE);
135        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
136        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
137        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
138        _zoneDAO = (ZoneDAO) manager.lookup(ZoneDAO.ROLE);
139        _scheduleOpeningHelper = (ScheduleOpeningHelper) manager.lookup(ScheduleOpeningHelper.ROLE);
140        _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
141        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
142        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
143        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
144        _copyFormEP = (CopyFormUpdaterExtensionPoint) manager.lookup(CopyFormUpdaterExtensionPoint.ROLE);
145    }
146
147    /**
148     * Check if a user have read rights on a form
149     * @param userIdentity the user
150     * @param form the form
151     * @return true if the user have read rights on a form
152     */
153    public boolean hasReadRightOnForm(UserIdentity userIdentity, Form form)
154    {
155        return _rightManager.hasReadAccess(userIdentity, form);
156    }
157    
158    /**
159     * Check if a user have write rights on a form element
160     * @param userIdentity the user
161     * @param formElement the form element
162     * @return true if the user have write rights on a form element
163     */
164    public boolean hasWriteRightOnForm(UserIdentity userIdentity, AmetysObject formElement)
165    {
166        return _rightManager.hasRight(userIdentity, HANDLE_FORMS_RIGHT_ID, formElement) == RightResult.RIGHT_ALLOW;
167    }
168    
169    /**
170     * Check if a user have write rights on a form
171     * @param userIdentity the user
172     * @param form the form
173     * @return true if the user have write rights on a form
174     */
175    public boolean hasRightAffectationRightOnForm(UserIdentity userIdentity, Form form)
176    {
177        return hasWriteRightOnForm(userIdentity, form) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW;
178    }
179    
180    /**
181     * Check rights for a form element as ametys object
182     * @param formElement the form element as ametys object
183     */
184    public void checkHandleFormRight(AmetysObject formElement)
185    {
186        UserIdentity user = _userProvider.getUser();
187        if (!hasWriteRightOnForm(user, formElement))
188        {
189            throw new IllegalAccessError("User '" + user + "' tried to handle forms without convenient right [" + HANDLE_FORMS_RIGHT_ID + "]");
190        }
191    }
192    
193    /**
194     * Get all forms from a site
195     * @param siteName the site name
196     * @return the list of form
197     */
198    public List<Form> getForms(String siteName)
199    {
200        String xpathQuery = "//element(" + siteName + ", ametys:site)//element(*, ametys:form)";
201        return _resolver.query(xpathQuery)
202            .stream()
203            .filter(Form.class::isInstance)
204            .map(Form.class::cast)
205            .collect(Collectors.toList());
206    }
207    
208    /**
209     * Get the form properties
210     * @param formId The form's id
211     * @param full <code>true</code> to get full information on form
212     * @param withRights <code>true</code> to have rights in the properties
213     * @return The form properties
214     */
215    @Callable
216    public Map<String, Object> getFormProperties (String formId, boolean full, boolean withRights)
217    {
218        try
219        {
220            Form form = _resolver.resolveById(formId);
221            return getFormProperties(form, full, true);
222        }
223        catch (UnknownAmetysObjectException e)
224        {
225            getLogger().warn("Can't find form with id: {}. It probably has just been deleted", formId, e);
226            Map<String, Object> infos = new HashMap<>();
227            infos.put("id", formId);
228            return infos;
229        }
230    }
231    
232    /**
233     * Get the form properties
234     * @param form The form
235     * @param full <code>true</code> to get full information on form
236     * @param withRights <code>true</code> to have rights in the properties
237     * @return The form properties
238     */
239    public Map<String, Object> getFormProperties (Form form, boolean full, boolean withRights)
240    {
241        Map<String, Object> infos = new HashMap<>();
242        
243        List<FormEntry> entries = form.getEntries();
244        List<SitemapElement> pages = getFormPage(form.getId(), form.getSiteName());
245        String workflowName = form.getWorkflowName();
246
247        infos.put("type", "root");
248        infos.put("isForm", true);
249        infos.put("author", _userHelper.user2json(form.getAuthor(), true)); 
250        infos.put("contributor", _userHelper.user2json(form.getContributor())); 
251        infos.put("lastModificationDate", DateUtils.zonedDateTimeToString(form.getLastModificationDate()));
252        infos.put("creationDate", DateUtils.zonedDateTimeToString(form.getCreationDate()));
253        infos.put("entriesAmount", entries.size());
254        infos.put("lastEntry", _getLastSubmissionDate(entries));
255        infos.put("workflowLabel", StringUtils.isNotBlank(workflowName) ? _workflowHelper.getWorkflowLabel(workflowName) : new I18nizableText("plugin.forms", "PLUGINS_FORMS_FORMS_EDITOR_WORKFLOW_NO_WORKFLOW"));
256        
257        /** Use in the bus message */
258        infos.put("id", form.getId());
259        infos.put("name", form.getName());
260        infos.put("title", form.getTitle());
261        infos.put("fullPath", getFormFullPath(form.getId()));
262        infos.put("pages", _getPagesInfos(pages));
263        infos.put("hasChildren", form.getPages().size() > 0);
264        infos.put("workflowName", workflowName);
265        
266        infos.put("isConfigured", isFormConfigured(form));
267        
268        UserIdentity currentUser = _userProvider.getUser();
269        if (withRights)
270        {
271            Set<String> userRights = _getUserRights(form);
272            infos.put("rights", userRights);
273            infos.put("canEditRight", userRights.contains(HANDLE_FORMS_RIGHT_ID) || _rightManager.hasRight(currentUser, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW);
274        }
275        else
276        {
277            boolean canWrite = hasWriteRightOnForm(currentUser, form);
278            infos.put("canWrite", canWrite);
279            infos.put("canEditRight", canWrite || _rightManager.hasRight(currentUser, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW);
280            infos.put("canRead", hasReadRightOnForm(currentUser, form));
281        }
282        
283        if (full)
284        {
285            infos.put("isPublished", !pages.isEmpty());
286            infos.put("hasEntries", form.hasEntries());
287            infos.put("nbEntries", form.getActiveEntries().size());
288            
289            FormDirectory formDirectoriesRoot = _formDirectoryDAO.getFormDirectoriesRootNode(form.getSiteName());
290            String parentId = form.getParent().getId().equals(formDirectoriesRoot.getId()) ? FormDirectoryDAO.ROOT_FORM_DIRECTORY_ID : form.getParent().getId();
291            infos.put("parentId", parentId);
292            
293            infos.put("isLimitedToOneEntryByUser", form.isLimitedToOneEntryByUser());
294            infos.put("isEntriesLimited", form.isEntriesLimited());
295            Optional<Long> maxEntries = form.getMaxEntries();
296            if (maxEntries.isPresent())
297            {
298                infos.put("maxEntries", maxEntries.get());
299            }
300            infos.put("isQueueEnabled", form.isQueueEnabled());
301            Optional<Long> queueSize = form.getQueueSize();
302            if (queueSize.isPresent())
303            {
304                infos.put("queueSize", queueSize.get());
305            }
306            
307            LocalDate startDate = form.getStartDate();
308            LocalDate endDate = form.getEndDate();
309            if (startDate != null || endDate != null)
310            {
311                infos.put("scheduleStatus", _scheduleOpeningHelper.getStatus(form));
312                if (startDate != null)
313                {
314                    infos.put("startDate", DateUtils.localDateToString(startDate));
315                }
316                if (endDate != null)
317                {
318                    infos.put("endDate", DateUtils.localDateToString(endDate));
319                }
320            }
321
322            infos.put("adminEmails", form.hasValue(Form.ADMIN_EMAIL_SUBJECT));
323            infos.put("receiptAcknowledgement", form.hasValue(Form.RECEIPT_SENDER));
324            infos.put("isAnonymous", _rightManager.hasAnonymousReadAccess(form));
325        }
326        
327        return infos;
328    }
329    
330    /**
331     * Get the form title
332     * @param formId the form id
333     * @return the form title
334     */
335    @Callable
336    public String getFormTitle(String formId)
337    {
338        Form form = _resolver.resolveById(formId);
339        return form.getTitle();
340    }
341    
342    /**
343     * Get the form full path
344     * @param formId the form id
345     * @return the form full path
346     */
347    @Callable
348    public String getFormFullPath(String formId)
349    {
350        Form form = _resolver.resolveById(formId);
351        
352        String separator = " > ";
353        String fullPath = form.getTitle();
354        
355        FormDirectory parent = form.getParent();
356        if (!_formDirectoryDAO.isRoot(parent))
357        {
358            fullPath = _formDirectoryDAO.getFormDirectoryPath(parent, separator) + separator + fullPath;
359        }
360        
361        return fullPath;
362    }
363    
364    /**
365     * Get user rights for the given form
366     * @param form the form
367     * @return the set of rights
368     */
369    protected Set<String> _getUserRights (Form form)
370    {
371        UserIdentity user = _userProvider.getUser();
372        return _rightManager.getUserRights(user, form);
373    }
374    
375    /**
376     * Creates a {@link Form}.
377     * @param siteName The site name
378     * @param parentId The id of the parent.
379     * @param name  name The desired name for the new {@link Form}
380     * @return The id of the created form
381     * @throws Exception if an error occurs during the form creation process
382     */
383    @Callable
384    public Map<String, String> createForm (String siteName, String parentId, String name) throws Exception
385    {
386        Map<String, String> result = new HashMap<>();
387        
388        FormDirectory parentDirectory = _formDirectoryDAO.getFormDirectory(siteName, parentId);
389        _formDirectoryDAO.checkHandleFormDirectoriesRight(parentDirectory);
390        
391        String uniqueName = _findUniqueName(parentDirectory, name);
392        Form form = parentDirectory.createChild(uniqueName, FormFactory.FORM_NODETYPE);
393        
394        form.setTitle(name);
395        form.setAuthor(_userProvider.getUser());
396        form.setCreationDate(ZonedDateTime.now());
397        form.setLastModificationDate(ZonedDateTime.now());
398        
399        parentDirectory.saveChanges();
400        String formId = form.getId();
401        
402        _formPageDAO.createPage(formId, _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGINS_FORMS_CREATE_PAGE_DEFAULT_NAME")));
403        
404        result.put("id", formId);
405        result.put("name", form.getTitle());
406        
407        return result;
408    }
409    
410    private String _findUniqueName(FormDirectory parent, String name)
411    {
412        String legalName;
413        try
414        {
415            legalName = NameHelper.filterName(name);
416        }
417        catch (IllegalArgumentException e)
418        {
419            legalName = NameHelper.filterName(__FORM_NAME_PREFIX + name);
420        }
421        
422        // Find unique name from legal name
423        String uniqueName = legalName;
424        int index = 2;
425        while (parent.hasChild(uniqueName))
426        {
427            uniqueName = legalName + "-" + (index++);
428        }
429        
430        return uniqueName;
431    }
432    
433    /**
434     * Rename a {@link Form}
435     * @param id The id of the form 
436     * @param newName The new name for the form
437     * @return A result map
438     */
439    @Callable
440    public Map<String, String> renameForm (String id, String newName)
441    {
442        Map<String, String> results = new HashMap<>();
443        
444        Form form = _resolver.resolveById(id);
445        checkHandleFormRight(form);
446        
447        String uniqueName = _findUniqueName(form.getParent(), newName);
448        Node node = form.getNode();
449        try
450        {
451            form.setTitle(newName);
452            form.setContributor(_userProvider.getUser());
453            form.setLastModificationDate(ZonedDateTime.now());
454            
455            node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + uniqueName);
456            node.getSession().save();
457            
458            results.put("newName", form.getTitle());
459        }
460        catch (RepositoryException e)
461        {
462            getLogger().warn("Form renaming failed.", e);
463            results.put("message", "cannot-rename");
464        }
465        
466        results.put("id", id);
467        return results;
468    }
469    
470    /**
471     * Copies and pastes a form.
472     * @param formDirectoryId The id of the form directory target of the copy
473     * @param formId The id of the form to copy
474     * @return The results
475     */
476    @Callable
477    public Map<String, String> copyForm(String formDirectoryId, String formId)
478    {
479        Map<String, String> result = new HashMap<>();
480        
481        Form originalForm = _resolver.resolveById(formId);
482        FormDirectory parentFormDirectory = _resolver.resolveById(formDirectoryId);
483        _formDirectoryDAO.checkHandleFormDirectoriesRight(parentFormDirectory);
484        
485        String uniqueName = _findUniqueName(parentFormDirectory, originalForm.getTitle());
486        
487        Form cForm = originalForm.copyTo(parentFormDirectory, uniqueName);
488        originalForm.copyTo(cForm);
489        
490        String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalForm.getTitle();
491        cForm.setTitle(copyTitle);
492        cForm.setAuthor(_userProvider.getUser());
493        cForm.setCreationDate(ZonedDateTime.now());
494        cForm.setLastModificationDate(ZonedDateTime.now());
495        
496        for (String epId : _copyFormEP.getExtensionsIds())
497        {
498            CopyFormUpdater copyFormUpdater = _copyFormEP.getExtension(epId);
499            copyFormUpdater.updateForm(originalForm, cForm);
500        }
501        
502        result.put("id", cForm.getId());
503        
504        return result;
505    }
506    
507    /**
508     * Deletes a {@link Form}.
509     * @param id The id of the form to delete
510     * @return The id of the form 
511     */
512    @Callable
513    public Map<String, String> deleteForm (String id)
514    {
515        Map<String, String> result = new HashMap<>();
516        
517        Form form = _resolver.resolveById(id);
518        checkHandleFormRight(form);
519        
520        List<SitemapElement> pages = getFormPage(form.getId(), form.getSiteName());
521        if (!pages.isEmpty())
522        {
523            throw new IllegalAccessError("Can't delete form ('" + form.getId() + "') which contains pages");
524        }
525
526        ModifiableAmetysObject parent = form.getParent();
527        form.remove();
528        parent.saveChanges();
529        
530        result.put("id", id);
531        return result;
532    }
533    
534    /**
535     * Moves a {@link Form}
536     * @param siteName name of the site
537     * @param id The id of the form
538     * @param newParentId The id of the new parent directory of the form. 
539     * @return A result map
540     */
541    @Callable
542    public Map<String, Object> moveForm(String siteName, String id, String newParentId)
543    {
544        Map<String, Object> results = new HashMap<>();
545        Form form = _resolver.resolveById(id);
546        FormDirectory directory = _formDirectoryDAO.getFormDirectory(siteName, newParentId);
547        
548        if (hasWriteRightOnForm(_userProvider.getUser(), form) && _formDirectoryDAO.hasWriteRightOnFormDirectory(_userProvider.getUser(), directory))
549        {
550            _formDirectoryDAO.move(form, siteName, newParentId, results);
551        }
552        else
553        {
554            results.put("message", "not-allowed");
555        }
556
557        results.put("id", form.getId());
558        return results;
559    }
560    
561    /**
562     * Change workflow of a {@link Form}
563     * @param formId The id of the form 
564     * @param workflowName The name of new workflow
565     * @return A result map
566     */
567    @Callable
568    public Map<String, String> setWorkflow (String formId, String workflowName)
569    {
570        Map<String, String> results = new HashMap<>();
571        
572        Form form = _resolver.resolveById(formId);
573        checkHandleFormRight(form);
574        
575        form.setWorkflowName(workflowName);
576        form.setContributor(_userProvider.getUser());
577        form.setLastModificationDate(ZonedDateTime.now());
578        
579        form.saveChanges();
580        
581        results.put("id", formId);
582
583        Map<String, Object> eventParams = new HashMap<>();
584        eventParams.put("form", form);
585        _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams));
586        
587        return results;
588    }
589
590    /**
591     * Get the submission date of the last entry to the form
592     * @param entries A list of form entry
593     * @return the date of the last submission
594     */
595    protected ZonedDateTime _getLastSubmissionDate(List<FormEntry> entries)
596    {
597        ZonedDateTime lastSubmissionDate = null;
598        if (!entries.isEmpty())
599        {
600            lastSubmissionDate = entries.get(0).getSubmitDate();
601            for (FormEntry entry : entries)
602            {
603                ZonedDateTime submitDate = entry.getSubmitDate();
604                if (lastSubmissionDate.isBefore(submitDate))
605                {
606                    lastSubmissionDate = submitDate;
607                }
608            }
609        }
610        return lastSubmissionDate;
611    }
612
613    /**
614     * Get all zone items which contains the form
615     * @param formId the form id
616     * @param siteName the site name
617     * @return the zone items
618     */
619    public AmetysObjectIterable<ModifiableZoneItem> getFormZoneItems(String formId, String siteName)
620    {
621        String xpathQuery = "//element(" + siteName + ", ametys:site)//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.forms.service.Display' and ametys:service_parameters/@ametys:formId = '" + formId + "']";
622        return _resolver.query(xpathQuery);
623    }
624    
625    /**
626     * Get the locale to use for a given form
627     * @param form the form
628     * @return the locale to use, can be null if the form is not published on a page
629     */
630    public String getFormLocale(Form form)
631    {
632        List<SitemapElement> zoneItems = getFormPage(form.getId(), form.getSiteName());
633        
634        return zoneItems.stream()
635                .findFirst()
636                .map(SitemapElement::getSitemapName)
637                .orElse(null);
638    }
639    
640    /**
641     * Get all the page where the form is published
642     * @param formId the form id
643     * @param siteName the site name
644     * @return the list of page
645     */
646    public List<SitemapElement> getFormPage(String formId, String siteName)
647    {
648        AmetysObjectIterable<ModifiableZoneItem> zoneItems = getFormZoneItems(formId, siteName);
649        
650        return zoneItems.stream()
651                .map(z -> z.getZone().getSitemapElement())
652                .collect(Collectors.toList());
653    }
654    
655    /**
656     * Get the page names
657     * @param pages the list of page
658     * @return the list of page name
659     */
660    protected List<Map<String, Object>> _getPagesInfos(List<SitemapElement> pages)
661    {
662        List<Map<String, Object>> pagesInfos = new ArrayList<>();
663        for (SitemapElement sitemapElement : pages)
664        {
665            Map<String, Object> info = new HashMap<>();
666            info.put("id", sitemapElement.getId());
667            info.put("title", sitemapElement.getTitle());
668            info.put("isPage", sitemapElement instanceof Page);
669            
670            pagesInfos.add(info);
671        }
672        
673        return pagesInfos;
674    }
675    
676    /**
677     * Get all the view available for the form display service
678     * @param formId the form identifier
679     * @param siteName the site name
680     * @param language the language
681     * @return the views as json
682     * @throws Exception if an error occurred
683     */
684    @Callable
685    public List<Map<String, Object>> getFormDisplayViews(String formId, String siteName, String language) throws Exception
686    {
687        List<Map<String, Object>> jsonifiedViews = new ArrayList<>();
688    
689        Service service = _serviceEP.getExtension("org.ametys.forms.service.Display");
690        ElementDefinition viewElementDefinition = (ElementDefinition) service.getParameters().getOrDefault(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME, null);
691        
692        String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + language
693                + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.forms.service.Display' and ametys:service_parameters/@ametys:formId = '" + formId + "']";
694        AmetysObjectIterable<ModifiableZoneItem> zoneItems = _resolver.query(xpathQuery);
695        
696        Optional<Object> existedServiceView = zoneItems.stream()
697            .map(ZoneItem::getServiceParameters)
698            .map(sp -> sp.getValue(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME))
699            .findFirst();
700        
701        Map<String, I18nizableText> typedEntries = viewElementDefinition.getEnumerator().getTypedEntries();
702        for (String id : typedEntries.keySet())
703        {
704            Map<String, Object> viewAsJson = new HashMap<>();
705            viewAsJson.put("id", id);
706            viewAsJson.put("label", typedEntries.get(id));
707            
708            Boolean isServiceView = existedServiceView.map(s -> s.equals(id)).orElse(false);
709            if (isServiceView || existedServiceView.isEmpty() && id.equals(viewElementDefinition.getDefaultValue()))
710            {
711                viewAsJson.put("isDefault", true);
712            }
713            
714            jsonifiedViews.add(viewAsJson);
715        }
716        
717        return jsonifiedViews;
718    }
719    
720    /**
721     * <code>true</code> if the form is well configured
722     * @param form the form
723     * @return <code>true</code> if the form is well configured
724     */
725    public boolean isFormConfigured(Form form)
726    {
727        List<FormQuestion> questions = form.getQuestions();
728        return !questions.isEmpty() && !questions.stream().anyMatch(q -> !q.getType().isQuestionConfigured(q));
729    }
730    
731    /**
732     * Get the dashboard URI
733     * @param siteName the site name
734     * @return the dashboard URI
735     */
736    public String getDashboardUri(String siteName)
737    {
738        String xpathQuery = "//element(" + siteName + ", ametys:site)//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.plugins.forms.workflow.service.dashboard']";
739        AmetysObjectIterable<ModifiableZoneItem> zoneItems = _resolver.query(xpathQuery);
740        
741        Optional<Page> dashboardPage = zoneItems.stream()
742                .map(z -> z.getZone().getSitemapElement())
743                .filter(Page.class::isInstance)
744                .map(Page.class::cast)
745                .findAny();
746        
747        if (dashboardPage.isPresent())
748        {
749            Page page = dashboardPage.get();
750            String pagePath = page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html";
751            
752            String url = _siteManager.getSite(siteName).getUrl();
753            return url + "/" + pagePath;
754        }
755        
756        return StringUtils.EMPTY;
757    }
758    
759    /**
760     * Get the admin dashboard URI
761     * @param siteName the site name
762     * @return the admin dashboard URI
763     */
764    public String getAdminDashboardUri(String siteName)
765    {
766        String xpathQuery = "//element(" + siteName + ", ametys:site)//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.plugins.forms.workflow.service.admin.dashboard']";
767        AmetysObjectIterable<ModifiableZoneItem> zoneItems = _resolver.query(xpathQuery);
768        
769        Optional<Page> dashboardPage = zoneItems.stream()
770                .map(z -> z.getZone().getSitemapElement())
771                .filter(Page.class::isInstance)
772                .map(Page.class::cast)
773                .findAny();
774        
775        if (dashboardPage.isPresent())
776        {
777            Page page = dashboardPage.get();
778            String pagePath = page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html";
779            
780            String url = _siteManager.getSite(siteName).getUrl();
781            return url + "/" + pagePath;
782        }
783        
784        return StringUtils.EMPTY;
785    }
786}