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