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