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