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