001/*
002 *  Copyright 2015 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.web.repository.page;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import javax.jcr.Node;
032import javax.jcr.Repository;
033import javax.jcr.RepositoryException;
034import javax.jcr.Session;
035
036import org.apache.avalon.framework.activity.Initializable;
037import org.apache.avalon.framework.component.Component;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.commons.lang3.StringUtils;
041
042import org.ametys.cms.contenttype.ContentType;
043import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
044import org.ametys.cms.languages.Language;
045import org.ametys.cms.repository.Content;
046import org.ametys.cms.repository.ContentDAO.TagMode;
047import org.ametys.cms.repository.ModifiableContent;
048import org.ametys.cms.repository.WorkflowAwareContent;
049import org.ametys.cms.tag.CMSTag;
050import org.ametys.cms.tag.CMSTag.TagVisibility;
051import org.ametys.cms.tag.Tag;
052import org.ametys.cms.tag.TagHelper;
053import org.ametys.cms.tag.TagProviderExtensionPoint;
054import org.ametys.core.cache.AbstractCacheManager;
055import org.ametys.core.cache.Cache;
056import org.ametys.core.observation.Event;
057import org.ametys.core.observation.ObservationManager;
058import org.ametys.core.observation.Observer;
059import org.ametys.core.right.RightManager;
060import org.ametys.core.right.RightManager.RightResult;
061import org.ametys.core.ui.Callable;
062import org.ametys.core.user.CurrentUserProvider;
063import org.ametys.core.user.UserIdentity;
064import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
065import org.ametys.plugins.explorer.ExplorerNode;
066import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
067import org.ametys.plugins.explorer.resources.Resource;
068import org.ametys.plugins.repository.AmetysObject;
069import org.ametys.plugins.repository.AmetysObjectIterable;
070import org.ametys.plugins.repository.AmetysObjectIterator;
071import org.ametys.plugins.repository.AmetysObjectResolver;
072import org.ametys.plugins.repository.AmetysRepositoryException;
073import org.ametys.plugins.repository.CopiableAmetysObject;
074import org.ametys.plugins.repository.ModifiableAmetysObject;
075import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
076import org.ametys.plugins.repository.RemovableAmetysObject;
077import org.ametys.plugins.repository.RepositoryConstants;
078import org.ametys.plugins.repository.TraversableAmetysObject;
079import org.ametys.plugins.repository.UnknownAmetysObjectException;
080import org.ametys.plugins.repository.jcr.JCRAmetysObject;
081import org.ametys.plugins.repository.jcr.NameHelper;
082import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
083import org.ametys.plugins.repository.lock.LockHelper;
084import org.ametys.plugins.repository.lock.LockableAmetysObject;
085import org.ametys.plugins.repository.query.expression.Expression.Operator;
086import org.ametys.plugins.repository.tag.TaggableAmetysObject;
087import org.ametys.plugins.repository.version.VersionableAmetysObject;
088import org.ametys.plugins.workflow.support.WorkflowProvider;
089import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
090import org.ametys.runtime.i18n.I18nizableText;
091import org.ametys.runtime.model.type.ModelItemTypeConstants;
092import org.ametys.web.ObservationConstants;
093import org.ametys.web.WebConstants;
094import org.ametys.web.alias.Alias.TargetType;
095import org.ametys.web.alias.AliasHelper;
096import org.ametys.web.alias.DefaultAlias;
097import org.ametys.web.languages.WebLanguagesManager;
098import org.ametys.web.repository.content.SharedContent;
099import org.ametys.web.repository.content.WebContent;
100import org.ametys.web.repository.content.shared.SharedContentManager;
101import org.ametys.web.repository.page.Page.LinkType;
102import org.ametys.web.repository.page.Page.PageType;
103import org.ametys.web.repository.page.ZoneItem.ZoneType;
104import org.ametys.web.repository.page.jcr.DefaultPage;
105import org.ametys.web.repository.site.Site;
106import org.ametys.web.repository.sitemap.Sitemap;
107import org.ametys.web.rights.PageRightAssignmentContext;
108import org.ametys.web.service.Service;
109import org.ametys.web.service.ServiceExtensionPoint;
110import org.ametys.web.site.CopyUpdaterExtensionPoint;
111import org.ametys.web.skin.Skin;
112import org.ametys.web.skin.SkinTemplate;
113import org.ametys.web.skin.SkinTemplateZone;
114import org.ametys.web.skin.SkinsManager;
115import org.ametys.web.skin.TemplatesAssignmentHandler;
116import org.ametys.web.synchronization.SynchronizeComponent;
117import org.ametys.web.tags.TagExpression;
118
119/**
120 * DAO for manipulating pages
121 */
122public class PageDAO extends AbstractSitemapElementsDAO implements Component, Initializable, Observer
123{
124    /** Constant for untouched binary metadata. */
125    public static final String __SERVICE_PARAM_UNTOUCHED_BINARY = "untouched";
126    
127    /** Avalon Role */
128    public static final String ROLE = PageDAO.class.getName();
129    
130    /** Constant for the {@link Cache} id for the pages in cache by sitename, lang, tag */
131    private static final String MEMORY_PAGESTAGCACHE = PageDAO.class.getName() + "$PagesUUIDByTag";
132    
133    private AmetysObjectResolver _resolver;
134    private ObservationManager _observationManager;
135    private CurrentUserProvider _currentUserProvider;
136    private SkinsManager _skinsManager;
137    private TemplatesAssignmentHandler _templatesHandler;
138    private ServicesAssignmentHandler _serviceHandler;
139    private ContentTypesAssignmentHandler _cTypeHandler;
140    private ContentTypeExtensionPoint _contentTypeExtensionPoint;
141    private ServiceExtensionPoint _serviceExtensionPoint;
142    private WorkflowProvider _workflowProvider;
143    private SharedContentManager _sharedContentManager;
144    private TagProviderExtensionPoint _tagProvider;
145    private WebLanguagesManager _webLanguagesManager;
146    private CopySiteComponent _copySiteComponent;
147    private RightManager _rightManager;
148    private SynchronizeComponent _synchronizeComponent;
149    private Repository _repository;
150    private AmetysObjectResolver _ametysObjectResolver;
151    private AbstractCacheManager _cacheManager;
152
153    private CopyUpdaterExtensionPoint _copyUpdaterEP;
154    
155    @Override
156    public void service(ServiceManager smanager) throws ServiceException
157    {
158        super.service(smanager);
159        
160        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
161        _ametysObjectResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
162        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
163        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
164        _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
165        _templatesHandler = (TemplatesAssignmentHandler) smanager.lookup(TemplatesAssignmentHandler.ROLE);
166        _cTypeHandler = (ContentTypesAssignmentHandler) smanager.lookup(ContentTypesAssignmentHandler.ROLE);
167        _serviceHandler = (ServicesAssignmentHandler) smanager.lookup(ServicesAssignmentHandler.ROLE);
168        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
169        _serviceExtensionPoint = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE);
170        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
171        _sharedContentManager = (SharedContentManager) smanager.lookup(SharedContentManager.ROLE);
172        _tagProvider = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE);
173        _webLanguagesManager = (WebLanguagesManager) smanager.lookup(WebLanguagesManager.ROLE);
174        _copySiteComponent = (CopySiteComponent) smanager.lookup(CopySiteComponent.ROLE);
175        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
176        _synchronizeComponent = (SynchronizeComponent) smanager.lookup(SynchronizeComponent.ROLE);
177        _repository = (Repository) smanager.lookup(Repository.class.getName());
178        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
179        _copyUpdaterEP = (CopyUpdaterExtensionPoint) smanager.lookup(CopyUpdaterExtensionPoint.ROLE);
180    }
181    
182    public void initialize() throws Exception
183    {
184        _createCaches();
185        _observationManager.registerObserver(this);
186    }
187    
188    private void _createCaches()
189    {
190        _cacheManager.createMemoryCache(MEMORY_PAGESTAGCACHE,
191                new I18nizableText("plugin.web", "PLUGINS_WEB_PAGEDAO_CACHE_PAGES_BY_TAG_LABEL"),
192                new I18nizableText("plugin.web", "PLUGINS_WEB_PAGEDAO_CACHE_PAGES_BY_TAG_DESCRIPTION"),
193                true,
194                null);
195    }
196    
197    public int getPriority(Event event)
198    {
199        return 0;
200    }
201    
202    public boolean supports(Event event)
203    {
204        return event.getId().equals(ObservationConstants.EVENT_PAGE_UPDATED) && event.getArguments().containsKey(ObservationConstants.ARGS_PAGE_TAGS);
205    }
206    
207    public void observe(Event event, Map<String, Object> transientVars) throws Exception
208    {
209        Page page = (Page) event.getArguments().get(ObservationConstants.ARGS_PAGE);
210        if (page != null)
211        {
212            _getMemoryPageTagCache().invalidate(PageTagCacheKey.of(page.getSiteName(), page.getSitemapName()));
213        }
214    }
215
216    /**
217     * Get the properties of given pages
218     * @param pageIds the id of pages
219     * @param zoneName The optional zone name to limit informations of zones to that zone. Can be null or empty to avoid limitation.
220     * @param zoneItemId The optional zone item identifier to limit informations of zones to that zone item. Can be null or empty to avoid limitation.
221     * @return the properties of pages in a result map
222     */
223    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
224    public Map<String, Object> getPagesInfos (List<String> pageIds, String zoneName, String zoneItemId)
225    {
226        // Assume that no read access is checked (required for bus message target)
227        Map<String, Object> result = new HashMap<>();
228        
229        List<Map<String, Object>> pages = new ArrayList<>();
230        List<String> pagesNotFound = new ArrayList<>();
231        
232        for (String pageId : pageIds)
233        {
234            try
235            {
236                Page page = _resolver.resolveById(pageId);
237                pages.add(getPageInfos(page, zoneName, zoneItemId));
238            }
239            catch (UnknownAmetysObjectException e)
240            {
241                pagesNotFound.add(pageId);
242            }
243        }
244        
245        result.put("pages", pages);
246        result.put("pagesNotFound", pagesNotFound);
247        
248        return result;
249    }
250    
251    /**
252     * Get the properties of given pages
253     * @param pageIds the id of pages
254     * @return the properties of pages in a result map
255     */
256    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
257    public Map<String, Object> getPagesInfos (List<String> pageIds)
258    {
259        // Assume that no read access is checked (required for bus message target)
260        return getPagesInfos(pageIds, null, null);
261    }
262    
263    /**
264     * Get the page's properties
265     * @param pageId the page ID
266     * @return the properties
267     */
268    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
269    public Map<String, Object> getPageInfos (String pageId)
270    {
271        // Assume that no read access is checked (required for bus message target)
272        return getPageInfos(pageId, null, null);
273    }
274    
275    /**
276     * Get the page's properties
277     * @param pageId the page ID
278     * @param zoneName The optional zone name to limit informations of zones to that zone. Can be null or empty to avoid limitation.
279     * @param zoneItemId The optional zone item identifier to limit informations of zones to that zone item. Can be null or empty to avoid limitation.
280     * @return the properties
281     */
282    public Map<String, Object> getPageInfos (String pageId, String zoneName, String zoneItemId)
283    {
284        // Assume that no read access is checked (required for bus message target)
285        Page page = _resolver.resolveById(pageId);
286        return getPageInfos(page, zoneName, zoneItemId);
287    }
288
289    /**
290     * Get the page's properties
291     * @param page the page
292     * @param zoneName The optional zone name to limit informations of zones to that zone. Can be null or empty to avoid limitation.
293     * @param zoneItemId The optional zone item identifier to limit informations of zones to that zone item. Can be null or empty to avoid limitation.
294     * @return the properties
295     */
296    public Map<String, Object> getPageInfos (Page page, String zoneName, String zoneItemId)
297    {
298        Map<String, Object> infos = new HashMap<>();
299        
300        infos.put("id", page.getId());
301        infos.put("name", page.getName());
302        infos.put("parentId", page.getParent().getId());
303        infos.put("title", page.getTitle());
304        infos.put("longTitle", page.getLongTitle());
305        infos.put("path", page.getPathInSitemap());
306        infos.put("siteName", page.getSiteName());
307        infos.put("type", page.getType());
308        infos.put("lang", page.getSitemapName());
309        infos.put("isModifiable", page instanceof ModifiablePage);
310        infos.put("isMoveable", page instanceof MoveablePage);
311        infos.put("isTaggable", page instanceof TaggableAmetysObject);
312        infos.put("isVisible", page.isVisible());
313        infos.put("isParentInvisible", _isParentInvisible(page));
314        // Publication information
315        infos.put("publication", _publication2Json(page));
316        infos.put("isPreviewable", _isPreviewable(page));
317        
318        // limitation information
319        if (StringUtils.isNotBlank(zoneName))
320        {
321            infos.put("zoneName", zoneName);
322        }
323        if (StringUtils.isNotBlank(zoneItemId))
324        {
325            infos.put("zoneItemId", zoneItemId);
326        }
327
328        String skinId = page.getSite().getSkinId();
329        Skin skin = _skinsManager.getSkin(skinId);
330        infos.put("isPageValid", _synchronizeComponent.isPageValid(page, skin));
331
332        Session liveSession = null;
333        try
334        {
335            liveSession = _repository.login(WebConstants.LIVE_WORKSPACE);
336            infos.put("isLiveHierarchyValid", _synchronizeComponent.isHierarchyValid(page, liveSession));
337        }
338        catch (RepositoryException e)
339        {
340            throw new RuntimeException("Unable to check live workspace", e);
341        }
342        finally
343        {
344            if (liveSession != null)
345            {
346                liveSession.logout();
347            }
348        }
349
350        PageType type = page.getType();
351        switch (type)
352        {
353            case CONTAINER:
354                infos.putAll(_sitemapElement2json(page, zoneName, zoneItemId));
355                break;
356            
357            case LINK:
358                infos.put("url", page.getURL());
359                
360                LinkType urlType = page.getURLType();
361                infos.put("urlType", urlType.toString());
362                
363                switch (urlType)
364                {
365                    case PAGE:
366                        try
367                        {
368                            Page targetPage = _resolver.resolveById(page.getURL());
369                            infos.put("urlTitle", targetPage.getTitle());
370                        }
371                        catch (UnknownAmetysObjectException e)
372                        {
373                            getLogger().error("Page '" + page.getId() + "' redirects to an unexisting page '" + page.getURL() + "'");
374                        }
375                        break;
376
377                    default:
378                        break;
379                }
380                break;
381            default:
382                AmetysObjectIterator< ? extends Page> iterator = page.getChildrenPages().iterator();
383                if (iterator.hasNext())
384                {
385                    Page firstSubPage = iterator.next();
386                    infos.put("url", firstSubPage.getId());
387                    infos.put("urlTitle", firstSubPage.getTitle());
388                    infos.put("urlType", LinkType.PAGE.toString());
389                    break;
390
391                }
392                break;
393        }
394        
395        
396        infos.put("rights", getUserRights(page));
397        
398        return infos;
399    }
400    
401    /**
402     * Get the page's properties
403     * @param page the page
404     * @return the properties
405     */
406    public Map<String, Object> getPageInfos (Page page)
407    {
408        return getPageInfos(page, null, null);
409    }
410    
411    /**
412     * Get the page's properties
413     * @param pageId the id of page
414     * @return the properties
415     */
416    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
417    public Map<String, Object> getPageProperties (String pageId)
418    {
419        Page page = _resolver.resolveById(pageId);
420        
421        if (!_rightManager.currentUserHasReadAccess(page))
422        {
423            return Map.of();
424        }
425        
426        Map<String, Object> infos = new HashMap<>();
427        
428        infos.put("id", page.getId());
429        if (page instanceof JCRAmetysObject)
430        {
431            try
432            {
433                String jcrId = ((JCRAmetysObject) page).getNode().getIdentifier();
434                infos.put("jcrId", jcrId);
435                
436                String jcrPath = ((JCRAmetysObject) page).getNode().getPath();
437                infos.put("jcrPath", jcrPath);
438            }
439            catch (RepositoryException e)
440            {
441                // Ignore
442            }
443        }
444        
445        infos.put("repository-access", Boolean.toString(_rightManager.currentUserHasRight("REPOSITORY_Rights_Access", "/repository") == RightResult.RIGHT_ALLOW));
446
447        infos.put("name", page.getName());
448        infos.put("parentId", page.getParent().getId());
449        infos.put("title", page.getTitle());
450        infos.put("longTitle", page.getLongTitle());
451        infos.put("path", page.getPathInSitemap());
452        infos.put("siteName", page.getSiteName());
453        infos.put("siteTitle", page.getSite().getTitle());
454        infos.put("type", page.getType());
455        
456        String lang = page.getSitemapName();
457        infos.put("lang", lang);
458        Language language = _webLanguagesManager.getAvailableLanguages().get(lang);
459        if (language != null)
460        {
461            infos.put("langIcon", language.getSmallIcon());
462            infos.put("langLabel", language.getLabel());
463        }
464        
465        PageType type = page.getType();
466        switch (type)
467        {
468            case CONTAINER:
469                infos.put("template", page.getTemplate());
470                break;
471            
472            case LINK:
473                infos.put("url", page.getURL());
474                LinkType urlType = page.getURLType();
475                infos.put("urlType", urlType.toString());
476                break;
477            default:
478                break;
479        }
480        
481        Map<String, Object> contextParameters = new HashMap<>();
482        contextParameters.put("siteName", page.getSiteName());
483        
484        // Tags
485        List<I18nizableText> tags = new ArrayList<>();
486        for (String tagName : page.getTags())
487        {
488            Tag tag = _tagProvider.getTag(tagName, contextParameters);
489            if (tag != null)
490            {
491                tags.add(tag.getTitle());
492            }
493        }
494        infos.put("tags", tags);
495        
496        // Incoming references
497        List<Map<String, Object>> incomingContents = new ArrayList<>();
498        AmetysObjectIterable<Content> contents = _getIncomingContentReferences (page.getId());
499        for (Content content : contents)
500        {
501            incomingContents.add(_content2Json(content, new Locale(page.getSitemapName())));
502        }
503        infos.put("inComingContents", incomingContents);
504        
505        List<Map<String, Object>> incomingPages = new ArrayList<>();
506        AmetysObjectIterable<Page> pages = _getIncomingPageReferences (page.getId());
507        for (Page pageRef : pages)
508        {
509            incomingPages.add(_page2Json(pageRef));
510        }
511        infos.put("inComingPages", incomingPages);
512        
513        // Publication information
514        infos.put("publication", _publication2Json(page));
515        
516        return infos;
517    }
518    
519    /**
520     * Check current user right on given page or sitemap
521     * @param id The id of the page or sitemap
522     * @param rightId The if of right to check
523     * @return true if user has right
524     */
525    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
526    public boolean hasRight(String id, String rightId)
527    {
528        UserIdentity user = _currentUserProvider.getUser();
529        SitemapElement page = _resolver.resolveById(id);
530        
531        return _rightManager.hasRight(user, rightId, page) == RightResult.RIGHT_ALLOW;
532    }
533    
534    /**
535     * Create a new page
536     * @param parentId The parent id. Can not be null.
537     * @param title The page's title. Can not be null.
538     * @param longTitle The page's long title. Can be null or blank.
539     * @return The result map with id of created page
540     */
541    @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
542    public Map<String, Object> createPage (String parentId, String title, String longTitle)
543    {
544        Map<String, Object> result = new HashMap<>();
545        
546        SitemapElement parent = _resolver.resolveById(parentId);
547        
548        assert parent instanceof ModifiableSitemapElement;
549        
550        try
551        {
552            ModifiablePage page = createPage((ModifiableSitemapElement) parent, null, title, longTitle);
553            result.put("id", page.getId());
554            result.put("parentId", parent.getId());
555            result.put("lang", page.getSitemapName());
556            result.put("title", page.getTitle());
557        }
558        catch (IllegalArgumentException e)
559        {
560            result.put("invalid-name", title);
561        }
562        return result;
563    }
564
565    /**
566     * Create a new page blank page.
567     * 
568     * In the case where {@code name} is {@code null}, the value of title will be used.
569     * In all case, the name (or title) will be used to derive the page name (and therefore
570     * path by filtering it with {@link NameHelper#filterName(String)} and appending an integer
571     * if a page with the same name already exist.
572     * 
573     * @param parent the sitemap element where the new page is created
574     * @param name the name to use as path for the page. (if null, the title will be used)
575     * @param title the title of the page
576     * @param longTitle the long title of the page
577     * @return The result map with id of the created page
578     * @throws IllegalArgumentException If the provided name (or title) can not be used in URI
579     */
580    public ModifiablePage createPage(ModifiableSitemapElement parent, String name, String title, String longTitle) throws IllegalArgumentException
581    {
582        Site site = parent.getSite();
583        
584        String pageName = PageDAO.findFreePageNameByTitle(parent, name != null ? name : title);
585        
586        ModifiablePage page = parent.createChild(pageName, "ametys:defaultPage");
587        
588        page.setTitle(title);
589        page.setType(PageType.NODE);
590        page.setSiteName(site.getName());
591        page.setSitemapName(page.getSitemap().getName());
592        
593        if (!StringUtils.isBlank(longTitle))
594        {
595            page.setLongTitle(longTitle);
596        }
597        
598        site.saveChanges();
599        
600        Map<String, Object> eventParams = new HashMap<>();
601        eventParams.put(ObservationConstants.ARGS_PAGE, page);
602        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams));
603        
604        return page;
605    }
606
607    /**
608     * Copy a page
609     * @param id The id of page to copy
610     * @param target The id of parent target page
611     * @param keepReferences true to keep references
612     * @return the result map
613     * @throws RepositoryException if an error occurred during copy
614     */
615    @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 1)
616    public Map<String, String> copyPage (String id, String target, boolean keepReferences) throws RepositoryException
617    {
618        Map<String, String> result = new HashMap<>();
619        
620        Page page = _resolver.resolveById(id);
621        
622        if (!(page instanceof CopiableAmetysObject))
623        {
624            throw new IllegalArgumentException("The page '" + page.getId() + "' is not a copiable ametys object, it can not be copied");
625        }
626        
627        if (!(page instanceof JCRAmetysObject))
628        {
629            throw new IllegalArgumentException("The page '" + page.getId() + "' is not a JCR ametys object, it can not be copied");
630        }
631        
632        SitemapElement parent = _resolver.resolveById(target);
633        
634        if (parent instanceof ModifiableSitemapElement modifiableParent)
635        {
636            ModifiablePage cPage = null;
637            
638            String initialTitle;
639            if (page instanceof DefaultPage defaultPage)
640            {
641                initialTitle = defaultPage.getInitialTitle();
642            }
643            else
644            {
645                initialTitle = page.getTitle();
646            }
647            
648            String newPageName = PageDAO.findFreePageNameByTitle(parent, initialTitle);
649            
650            if (!keepReferences)
651            {
652                // Restrict the copy to the page and its current children to avoid infinitive loop
653                List<String> pagesToCopy = new ArrayList<>();
654                pagesToCopy.add(page.getId());
655                pagesToCopy.addAll(_getChildrenPageIds(page));
656                
657                // Copy and duplicate contents
658                cPage = (ModifiablePage) ((CopiableAmetysObject) page).copyTo(modifiableParent, newPageName, pagesToCopy);
659                _copySiteComponent.updateReferencesAfterCopy(page, cPage);
660                
661                // Creates the first version on all copied contents
662                _updateContentsAfterCopy(page, cPage);
663            }
664            else
665            {
666                // Copy without duplicating contents (keep references)
667                
668                if (!page.getSitemapName().equals(modifiableParent.getSitemapName()))
669                {
670                    throw new IllegalArgumentException("The page '" + page.getId() + "' from sitemap '" + page.getSitemapName() + "' cannot be copied to a different sitemap '" + parent.getSitemapName() + "' while keeping references to contents");
671                }
672                
673                
674                String pagePath = ((SimpleAmetysObject) parent).getNode().getPath() + "/" + newPageName;
675                Node node = ((JCRAmetysObject) page).getNode();
676                node.getSession().getWorkspace().copy(node.getPath(), pagePath);
677                
678                cPage = modifiableParent.getChild(newPageName);
679            }
680            
681            cPage.setTitle(PageDAO.findFreeTitle(modifiableParent, initialTitle));
682            if (cPage instanceof DefaultPage defaultPage)
683            {
684                defaultPage.setInitialTitle(initialTitle);
685            }
686            
687            ((ModifiableTraversableAmetysObject) parent).saveChanges();
688            result.put("id", cPage.getId());
689            
690            Map<String, Object> eventParams = new HashMap<>();
691            eventParams.put(ObservationConstants.ARGS_PAGE, cPage);
692            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams));
693        }
694       
695        return result;
696    }
697
698    /**
699     * find a free name under a given parent node
700     * @param parent The parent
701     * @param initialTitle The title of the page
702     * @return The free name
703     */
704    public static String findFreePageNameByTitle(TraversableAmetysObject parent, String initialTitle)
705    {
706        return findFreePageNameByTitle(parent, initialTitle, null);
707    }
708    
709    /**
710     * find a free name under a given parent node
711     * @param parent The parent
712     * @param initialTitle The title of the page
713     * @param currentName The current name of the node, that can thus be authorized
714     * @return The free name
715     */
716    public static String findFreePageNameByTitle(TraversableAmetysObject parent, String initialTitle, String currentName)
717    {
718        String originalPageName = NameHelper.filterName(initialTitle);
719        
720        return findFreePageName(parent, originalPageName, currentName);
721    }
722
723    /**
724     * find a free name under a given parent node
725     * @param parent The parent
726     * @param originalPageName The name of the page
727     * @return The free name
728     */
729    public static String findFreePageName(TraversableAmetysObject parent, String originalPageName)
730    {
731        return findFreePageName(parent, originalPageName, null);
732    }
733    
734    /**
735     * find a free name under a given parent node
736     * @param parent The parent
737     * @param originalPageName The name of the page
738     * @param currentName The current name of the node, that can thus be authorized
739     * @return The free name
740     */
741    public static String findFreePageName(TraversableAmetysObject parent, String originalPageName, String currentName)
742    {
743        int index = 2;
744        
745        String pageName = originalPageName;
746        while (parent.hasChild(pageName) && !pageName.equals(currentName))
747        {
748            // Find unused name on new parent node
749            pageName = originalPageName + "-" + index++;
750        }
751        return pageName;
752    }
753    
754    /**
755     * find a free title under a given parent node
756     * @param parent The parent
757     * @param initialTitle The title of the page
758     * @return The free title
759     */
760    public static String findFreeTitle(SitemapElement parent, String initialTitle)
761    {
762        List<String> existingTitles = parent.getChildrenPages().stream().map(Page::getTitle).toList();
763        
764        int index = 2;
765        
766        String title = initialTitle;
767        while (existingTitles.contains(title))
768        {
769            title = initialTitle + " (" + index++ + ")";
770        }
771        
772        return title;
773    }
774    
775    /**
776     * Copy a page under another page
777     * @param targetId the page to copy in
778     * @param sourceId the page to copy
779     * @param keepReferences true to keep references for contents, or false to duplicate contents
780     * @return The id of created page is a result map.
781     * @throws RepositoryException if an error occurs
782     */
783    @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
784    @Deprecated
785    public Map<String, String> pastePage(String targetId, String sourceId, boolean keepReferences) throws RepositoryException
786    {
787        return copyPage(sourceId, targetId, keepReferences);
788    }
789    
790    /**
791     * Move a page
792     * @param id The page id
793     * @param parentId The id of parent destination
794     * @param index The position in parent child nodes where page will be inserted. -1 means as the last child.
795     * @return the result map
796     */
797    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
798    public Map<String, Object> movePage (String id, String parentId, int index)
799    {
800        Map<String, Object> result = new HashMap<>();
801        
802        String oldPathInSitemap = null;
803        String newPathInSitemap = null;
804     
805        Page page = _resolver.resolveById(id);
806        SitemapElement srcParent = page.getParent();
807        oldPathInSitemap = page.getPathInSitemap();
808        Sitemap sitemap = page.getSitemap();
809        
810        // check rights of deletion on old parent page
811        if (!srcParent.getId().equals(parentId) && !_canDelete(page))
812        {
813            throw new IllegalStateException("You do not have the rights to delete the page '/" + page.getSitemapName() + "/" + oldPathInSitemap + "'");
814        }
815        
816        if (!(page instanceof MoveablePage))
817        {
818            throw new IllegalArgumentException("The page '/" + page.getSitemapName() + "/" + oldPathInSitemap + "' is not a moveable page");
819        }
820        
821        if (srcParent.getId().equals(parentId) && index != -1)
822        {
823            try
824            {
825                // in case of reorder, check right of creation on current parent page
826                if (!_canCreate(srcParent))
827                {
828                    throw new IllegalStateException("You do not have to reorder a page under '/" + srcParent.getSitemapName() + "/" + srcParent.getPathInSitemap() + "'");
829                }
830                
831                Page brother = srcParent.getChildPageAt(index);
832                ((MoveablePage) page).orderBefore(brother);
833            }
834            catch (UnknownAmetysObjectException e)
835            {
836                // Move the last child position
837                ((MoveablePage) page).orderBefore(null);
838            }
839            
840            // Path is not modified
841            newPathInSitemap = oldPathInSitemap;
842        }
843        else
844        {
845            SitemapElement newParentPage = _resolver.resolveById(parentId);
846
847            // check right of creation on new parent page
848            if (!_canCreate(newParentPage))
849            {
850                throw new IllegalStateException("You do not have the rights to create a page under '/" + newParentPage.getSitemapName() + "/" + newParentPage.getPathInSitemap() + "'");
851            }
852
853            ((MoveablePage) page).moveTo(newParentPage, true);
854            if (index != -1)
855            {
856                Page brother = newParentPage.getChildPageAt(index);
857                
858                ((MoveablePage) page).orderBefore(brother);
859            }
860            
861            // Path is modified
862            newPathInSitemap = page.getPathInSitemap();
863        }
864        
865        if (sitemap.needsSave())
866        {
867            sitemap.saveChanges();
868        }
869
870        // Notify observers that the page has been moved
871        Map<String, Object> eventParams = new HashMap<>();
872        eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
873        eventParams.put(ObservationConstants.ARGS_PAGE, page);
874        eventParams.put("page.old.path", oldPathInSitemap);
875        eventParams.put("page.old.parent", srcParent);
876        eventParams.put(ObservationConstants.ARGS_PAGE_PATH, newPathInSitemap);
877        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_MOVED, _currentUserProvider.getUser(), eventParams));
878        
879        result.put("id", page.getId());
880        result.put("parentId", page.getParent().getId());
881        
882        return result;
883    }
884    
885    private boolean _canCreate(SitemapElement parentPage)
886    {
887        UserIdentity user = _currentUserProvider.getUser();
888        if (_rightManager.hasRight(user, "Web_Rights_Page_Create", parentPage) == RightResult.RIGHT_ALLOW)
889        {
890            return true;
891        }
892        
893        if (getLogger().isInfoEnabled())
894        {
895            getLogger().info("The user '" + user + "' tried to create page under '/" + parentPage.getSitemapName() + "/" + parentPage.getPathInSitemap() + "' without sufficient rights");
896        }
897        
898        return false;
899    }
900    
901    private boolean _canDelete(Page page)
902    {
903        UserIdentity user = _currentUserProvider.getUser();
904        SitemapElement parent = page.getParent();
905        if (_rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW)
906        {
907            return true;
908        }
909        
910        if (getLogger().isInfoEnabled())
911        {
912            getLogger().info("The user '" + user + "' tried to move page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' without sufficient rights");
913        }
914        
915        return false;
916    }
917    
918    /**
919     * Set pages as redirection
920     * @param pageIds the id of pages to modify
921     * @param url the url of redirection
922     * @param urlType the type of redirection
923     * @return the id of pages which succeeded or failed.
924     */
925    @Callable (rights =  Callable.SKIP_BUILTIN_CHECK)
926    public Map<String, Object> setLink (List<String> pageIds, String url, String urlType)
927    {
928        List<String> allRightIds = new ArrayList<>();
929        List<Map<String, Object>> noRightPages = new ArrayList<>();
930        List<Map<String, Object>> errorPages = new ArrayList<>();
931        List<Map<String, Object>> noModifiablePages = new ArrayList<>();
932        
933        if (StringUtils.isEmpty(url))
934        {
935            throw new IllegalArgumentException("Can not set page as a redirection with an empty url");
936        }
937        
938        for (String pageId : pageIds)
939        {
940            Page page = _resolver.resolveById(pageId);
941            
942            try
943            {
944                if (_rightManager.currentUserHasRight("Web_Rights_Page_LinkPage", page) != RightResult.RIGHT_ALLOW)
945                {
946                    noRightPages.add(Map.of("id", pageId, "title", page.getTitle()));
947                }
948                else if (!(page instanceof ModifiablePage))
949                {
950                    noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle()));
951                }
952                else
953                {
954                    ModifiablePage mPage = (ModifiablePage) page;
955                    
956                    if (page.getType().equals(PageType.CONTAINER))
957                    {
958                        // Remove zones
959                        for (ModifiableZone zone : mPage.getZones())
960                        {
961                            zone.remove();
962                        }
963                    }
964                    
965                    if (pageId.equals(url))
966                    {
967                        throw new IllegalArgumentException("A page can not redirect to itself");
968                    }
969                    
970                    mPage.setType(PageType.LINK);
971                    mPage.setURL(LinkType.valueOf(urlType), url);
972                    mPage.getSitemap().saveChanges();
973                    
974                    allRightIds.add(pageId);
975                    
976                    Map<String, Object> eventParams = new HashMap<>();
977                    eventParams.put(ObservationConstants.ARGS_PAGE, page);
978                    _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
979                }
980            }
981            catch (Exception e)
982            {
983                getLogger().error("Cannot set the page '" + pageId + "' as link [" + url + ", " + urlType.toString() + "]", e);
984                errorPages.add(Map.of("id", pageId, "title", page.getTitle()));
985            }
986        }
987
988        return Map.of("allright-pages", allRightIds, "noright-pages", noRightPages, "error-pages", errorPages, "nomodifiable-pages", noModifiablePages);
989    }
990    
991    /**
992     * Get available template for specified page
993     * @param pageId The page's id
994     * @return the list of available template as JSON
995     */
996    private List<Map<String, Object>> _getAvailableTemplates (String pageId)
997    {
998        List<Map<String, Object>> templates = new ArrayList<>();
999        
1000        Page page = _resolver.resolveById(pageId);
1001        
1002        Set<String> availableTemplateIds = _templatesHandler.getAvailablesTemplates(page);
1003        for (String templateName : availableTemplateIds)
1004        {
1005            String skinId = page.getSite().getSkinId();
1006            Skin skin = _skinsManager.getSkin(skinId);
1007            
1008            SkinTemplate template = skin.getTemplate(templateName);
1009            
1010            Map<String, Object> template2json = new HashMap<>();
1011            template2json.put("id", template.getId());
1012            template2json.put("label", template.getLabel());
1013            template2json.put("description", template.getDescription());
1014            template2json.put("iconSmall", template.getSmallImage());
1015            template2json.put("iconMedium", template.getMediumImage());
1016            template2json.put("iconLarge", template.getLargeImage());
1017            template2json.put("zone", template.getDefaultZoneId());
1018            
1019            templates.add(template2json);
1020        }
1021        
1022        return templates;
1023    }
1024    
1025    /**
1026     * Get available content types for specified page
1027     * @param pageId The page's id
1028     * @param zoneName the name of the zone
1029     * @return the list of available content types
1030     */
1031    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1032    public List<Map<String, Object>> getAvailableContentTypes (String pageId, String zoneName)
1033    {
1034        List<Map<String, Object>> contenttypes = new ArrayList<>();
1035        
1036        Page page = _resolver.resolveById(pageId);
1037        
1038        Set<String> contentTypeIds = _cTypeHandler.getAvailableContentTypes(page, zoneName);
1039        for (String contentTypeId : contentTypeIds)
1040        {
1041            ContentType cType = _contentTypeExtensionPoint.getExtension(contentTypeId);
1042            
1043            if (cType != null && _hasRight(cType, page))
1044            {
1045                Map<String, Object> ctype2json = new HashMap<>();
1046                ctype2json.put("id", cType.getId());
1047                ctype2json.put("label", cType.getLabel());
1048                ctype2json.put("description", cType.getDescription());
1049                ctype2json.put("iconGlyph", cType.getIconGlyph());
1050                ctype2json.put("iconDecorator", cType.getIconDecorator());
1051                ctype2json.put("iconSmall", cType.getSmallIcon());
1052                ctype2json.put("iconMedium", cType.getMediumIcon());
1053                ctype2json.put("iconLarge", cType.getLargeIcon());
1054                ctype2json.put("defaultTitle", cType.getDefaultTitle());
1055                ctype2json.put("viewNames", cType.getViewNames(true));
1056                ctype2json.put("category", cType.getCategory().isI18n() ? cType.getCategory().getKey() : cType.getCategory().getLabel());
1057                ctype2json.put("categoryLabel", cType.getCategory());
1058                
1059                contenttypes.add(ctype2json);
1060            }
1061        }
1062        
1063        return contenttypes;
1064    }
1065    
1066    /**
1067     * Get available content types for a page being created
1068     * @param pageId The page's id. Can be null of the page is not yet created
1069     * @param zoneName the name of the zone
1070     * @param parentId The id of parent page
1071     * @param pageTitle The title of page to create
1072     * @param template The template of page to create
1073     * @return the list of available services
1074     */
1075    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1076    public List<Map<String, Object>> getAvailableContentTypesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template)
1077    {
1078        if (StringUtils.isNotEmpty(pageId))
1079        {
1080            // Get available services for a page
1081            return getAvailableContentTypes(pageId, zoneName);
1082        }
1083        else if (StringUtils.isNotEmpty(parentId))
1084        {
1085            // Get available services for a not yet existing page
1086            SitemapElement parent = _resolver.resolveById(parentId);
1087            
1088            // Create page temporarily
1089            Page page = _createPage(parent, pageTitle, template);
1090            
1091            List<Map<String, Object>> availableContentTypes = getAvailableContentTypes(page.getId(), zoneName);
1092            
1093            // Cancel page creation
1094            page.getSitemap().revertChanges();
1095            
1096            return availableContentTypes;
1097        }
1098        
1099        return Collections.EMPTY_LIST;
1100    }
1101    
1102    private boolean _hasRight(ContentType contentType, Page page)
1103    {
1104        String right = contentType.getRight();
1105        
1106        if (right == null)
1107        {
1108            return true;
1109        }
1110        else
1111        {
1112            UserIdentity user = _currentUserProvider.getUser();
1113            return _rightManager.hasRight(user, right, page) == RightResult.RIGHT_ALLOW;
1114        }
1115    }
1116    
1117    /**
1118     * Get available services for specified page
1119     * @param pageId The page's id
1120     * @param zoneName the name of the zone
1121     * @return the list of available services
1122     */
1123    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1124    public List<Map<String, Object>> getAvailableServices (String pageId, String zoneName)
1125    {
1126        List<Map<String, Object>> services = new ArrayList<>();
1127        
1128        SitemapElement sitemapElement = _resolver.resolveById(pageId);
1129        
1130        Set<String> serviceIds = _serviceHandler.getAvailableServices(sitemapElement, zoneName);
1131        for (String serviceId : serviceIds)
1132        {
1133            Service service = _serviceExtensionPoint.getExtension(serviceId);
1134            if (service != null && _hasRight(service, sitemapElement))
1135            {
1136                Map<String, Object> serviceMap = new HashMap<>();
1137                serviceMap.put("id", service.getId());
1138                serviceMap.put("label", service.getLabel());
1139                serviceMap.put("description", service.getDescription());
1140                serviceMap.put("iconGlyph", service.getIconGlyph());
1141                serviceMap.put("iconDecorator", service.getIconDecorator());
1142                serviceMap.put("iconSmall", service.getSmallIcon());
1143                serviceMap.put("iconMedium", service.getMediumIcon());
1144                serviceMap.put("iconLarge", service.getLargeIcon());
1145                serviceMap.put("parametersAction", service.getParametersScript().getScriptClassname());
1146                serviceMap.put("category", service.getCategory().isI18n() ? service.getCategory().getKey() : service.getCategory().getLabel());
1147                serviceMap.put("categoryLabel", service.getCategory());
1148                
1149                services.add(serviceMap);
1150            }
1151        }
1152        
1153        return services;
1154    }
1155    
1156    /**
1157     * Get available services for a page being created
1158     * @param pageId The page's id. Can be null of the page is not yet created
1159     * @param zoneName the name of the zone
1160     * @param parentId The id of parent page
1161     * @param pageTitle The title of page to create
1162     * @param template The template of page to create
1163     * @return the list of available services
1164     */
1165    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1166    public List<Map<String, Object>> getAvailableServicesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template)
1167    {
1168        if (StringUtils.isNotEmpty(pageId))
1169        {
1170            // Get available services for a page
1171            return getAvailableServices(pageId, zoneName);
1172        }
1173        else if (StringUtils.isNotEmpty(parentId))
1174        {
1175            // Get available services for a not yet existing page
1176            SitemapElement parent = _resolver.resolveById(parentId);
1177            
1178            // Create page temporarily
1179            Page page = _createPage(parent, pageTitle, template);
1180            
1181            List<Map<String, Object>> availableServices = getAvailableServices(page.getId(), zoneName);
1182            
1183            // Cancel page creation
1184            page.getSitemap().revertChanges();
1185            
1186            return availableServices;
1187        }
1188        
1189        return Collections.EMPTY_LIST;
1190    }
1191    
1192    private Page _createPage (SitemapElement parent, String pageTitle, String template)
1193    {
1194        Site site = parent.getSite();
1195        
1196        String pageName = PageDAO.findFreePageNameByTitle(parent, pageTitle);
1197        
1198        ModifiablePage page = ((ModifiableTraversableAmetysObject) parent).createChild(pageName, "ametys:defaultPage");
1199        
1200        page.setTitle(pageTitle);
1201        page.setType(PageType.NODE);
1202        page.setSiteName(site.getName());
1203        page.setSitemapName(page.getSitemap().getName());
1204
1205        if (template != null)
1206        {
1207            String skinId = page.getSite().getSkinId();
1208            SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(template);
1209            if (tpl == null)
1210            {
1211                throw new IllegalStateException("Template '" + template + "' does not exist on skin '" + skinId + "'");
1212            }
1213            
1214            // Set temporary the template to get available services
1215            page.setType(PageType.CONTAINER);
1216            page.setTemplate(template);
1217        }
1218        
1219        return page;
1220    }
1221    
1222    private boolean _hasRight(Service service, SitemapElement sitemapElement)
1223    {
1224        String right = service.getRight();
1225        
1226        if (right == null)
1227        {
1228            return true;
1229        }
1230        else
1231        {
1232            UserIdentity user = _currentUserProvider.getUser();
1233            return _rightManager.hasRight(user, right, sitemapElement) == RightResult.RIGHT_ALLOW;
1234        }
1235    }
1236    
1237    /**
1238     * Get available content types for a page being created
1239     * @param pageId The page's id. Can be null of the page is not yet created
1240     * @param parentId The id of parent page
1241     * @param pageTitle The title of page to create
1242     * @return the list of available services
1243     */
1244    @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 1)
1245    public List<Map<String, Object>> getAvailableTemplatesForCreation (String pageId, String parentId, String pageTitle)
1246    {
1247        if (StringUtils.isNotEmpty(pageId))
1248        {
1249            // Get available template for a page
1250            return _getAvailableTemplates(pageId);
1251        }
1252        else if (StringUtils.isNotEmpty(parentId))
1253        {
1254            // Get available template for a not yet existing page
1255            SitemapElement parent = _resolver.resolveById(parentId);
1256            
1257            // Create page temporarily
1258            Page page = _createPage(parent, pageTitle, null);
1259            
1260            List<Map<String, Object>> availableTemplates = _getAvailableTemplates(page.getId());
1261            
1262            // Cancel page creation
1263            page.getSitemap().revertChanges();
1264            
1265            return availableTemplates;
1266        }
1267        
1268        return Collections.EMPTY_LIST;
1269    }
1270    
1271    /**
1272     * Get service info
1273     * @param pageId Optional, the page id of the service. To get some basic info about the page.
1274     * @param serviceId The id of the service
1275     * @return a Map containing some info about the service (label, url..)
1276     */
1277    @Callable (rights = Callable.READ_ACCESS, rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
1278    public Map<String, Object> getServiceInfo(String pageId, String serviceId)
1279    {
1280        Map<String, Object> info = new HashMap<>();
1281        
1282        if (StringUtils.isNotEmpty(pageId))
1283        {
1284            SitemapElement sitemapElement = _resolver.resolveById(pageId);
1285            info.put("page-id", sitemapElement.getId());
1286            info.put("page-title", sitemapElement.getTitle());
1287        }
1288
1289        Service service = _serviceExtensionPoint.getExtension(serviceId);
1290        info.put("id", service.getId());
1291        info.put("label", service.getLabel());
1292        info.put("url", service.getURL());
1293        info.put("smallIcon", service.getSmallIcon());
1294        info.put("iconGlyph", service.getIconGlyph());
1295        info.put("iconDecorator", service.getIconDecorator());
1296        return info;
1297    }
1298    
1299    /**
1300     * Rename a page
1301     * @param pageId The id of page to rename
1302     * @param title The page's  title
1303     * @param longTitle The page's long title.
1304     * @param updatePath true to update page's path
1305     * @param createAlias true to create a alias
1306     * @return the result map
1307     */
1308    @Callable (rights = "Web_Rights_Page_Rename", rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
1309    public Map<String, Object> renamePage (String pageId, String title, String longTitle, boolean updatePath, boolean createAlias)
1310    {
1311        Map<String, Object> result = new HashMap<>();
1312        
1313        Page page = _resolver.resolveById(pageId);
1314        
1315        if (!(page instanceof ModifiablePage))
1316        {
1317            throw new IllegalArgumentException("Can not rename a non-modifiable page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1318        }
1319        
1320        ModifiablePage mPage = (ModifiablePage) page;
1321        mPage.setTitle(title);
1322        mPage.setLongTitle(longTitle);
1323
1324        if (updatePath)
1325        {
1326            String oldPathInSitemap = page.getPathInSitemap();
1327            String oldPath = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html";
1328            String oldPathForChild = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/**.html";
1329
1330            String pageName = "";
1331            try
1332            {
1333                pageName = PageDAO.findFreePageNameByTitle(mPage.getParent(), title, page.getName());
1334            }
1335            catch (IllegalArgumentException e)
1336            {
1337                result.put("invalid-name", title);
1338                return result;
1339            }
1340            
1341            if (!page.getName().equals(pageName))
1342            {
1343                mPage.rename(pageName);
1344                
1345                if (createAlias)
1346                {
1347                    ModifiableTraversableAmetysObject rootNode = AliasHelper.getRootNode(page.getSite());
1348                    
1349                    DefaultAlias alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias");
1350                    alias.setUrl(oldPath);
1351                    alias.setTarget(page.getId());
1352                    alias.setType(TargetType.PAGE);
1353                    alias.setCreationDate(new Date());
1354
1355                    // Alias for child pages
1356                    alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias");
1357                    alias.setUrl(oldPathForChild);
1358                    alias.setTarget("/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/{1}.html");
1359                    alias.setType(TargetType.URL);
1360                    alias.setCreationDate(new Date());
1361
1362                    rootNode.saveChanges();
1363                }
1364                
1365                // Notify observers that the page has been renamed
1366                Map<String, Object> eventParams = new HashMap<>();
1367                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1368                eventParams.put("path.old.path", oldPathInSitemap);
1369                eventParams.put(ObservationConstants.ARGS_PAGE_PATH, page.getPathInSitemap());
1370                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_RENAMED, _currentUserProvider.getUser(), eventParams));
1371                
1372            }
1373            else
1374            {
1375                // Notify observers that the page's title has been modified
1376                Map<String, Object> eventParams = new HashMap<>();
1377                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1378                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1379            }
1380                
1381        }
1382        else
1383        {
1384            // Notify observers that the page's title has been modified
1385            Map<String, Object> eventParams = new HashMap<>();
1386            eventParams.put(ObservationConstants.ARGS_PAGE, page);
1387            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1388        }
1389        
1390        Sitemap sitemap = page.getSitemap();
1391        if (sitemap.needsSave())
1392        {
1393            sitemap.saveChanges();
1394        }
1395        
1396        result.put("id", page.getId());
1397        result.put("path", page.getPath());
1398        result.put("title", page.getTitle());
1399        
1400        return result;
1401    }
1402    
1403    /**
1404     * Delete a page and its sub-pages.
1405     * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true.
1406     * The newly created contents are deleted whatever the value if 'deleteBelongingContents'.
1407     * @param pageId the id of page to delete
1408     * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only
1409     * @return The id of deleted pages
1410     */
1411    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1412    public Map<String, Object> deletePage(String pageId, boolean deleteBelongingContents)
1413    {
1414        ModifiablePage page = _resolver.resolveById(pageId);
1415        
1416        // Check rights on parent
1417        if (!_canDelete(page))
1418        {
1419            throw new IllegalStateException("You do not have the rights to delete the page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1420        }
1421        
1422        return deletePage(page, deleteBelongingContents);
1423    }
1424    
1425    /**
1426     * Delete a page and its sub-pages.
1427     * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true.
1428     * The newly created contents are deleted whatever the value if 'deleteBelongingContents'.
1429     * @param page the page to delete
1430     * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only
1431     * @return The id of deleted pages
1432     */
1433    public Map<String, Object> deletePage(ModifiablePage page, boolean deleteBelongingContents)
1434    {
1435        Map<String, Object> result = new HashMap<>();
1436        
1437        List<String> contentToDelete = getDeleteablePageContentIds(page.getId(), !deleteBelongingContents);
1438        
1439        Sitemap sitemap = page.getSitemap();
1440        SitemapElement parent = page.getParent();
1441        String pagePathInSitemap = page.getPathInSitemap();
1442        
1443        List<String> childPagesIds = _getChildrenPageIds(page);
1444        
1445        Map<String, Object> eventParams = new HashMap<>();
1446        eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1447        eventParams.put(ObservationConstants.ARGS_PAGE_PARENT, parent);
1448        eventParams.put(ObservationConstants.ARGS_PAGE_PATH, pagePathInSitemap);
1449        eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
1450        eventParams.put(ObservationConstants.ARGS_PAGE_CONTENTS, getPageContents(page));
1451        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETING, _currentUserProvider.getUser(), eventParams));
1452        
1453        // FIXME API test if this is not modifiable
1454        page.getParent();
1455        page.remove();
1456        ((ModifiableAmetysObject) parent).saveChanges();
1457        
1458        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETED, _currentUserProvider.getUser(), eventParams));
1459        
1460        result.put("id", page.getId());
1461        result.put("childPages", childPagesIds);
1462        
1463        result.putAll(_contentDAO.deleteContents(contentToDelete, true));
1464        return result;
1465    }
1466    
1467    /**
1468     * Set pages as blank page
1469     * @param pageIds the id of pages to modify
1470     * @return the id of pages which succeeded or failed.
1471     */
1472    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1473    public Map<String, Object> setBlank (List<String> pageIds)
1474    {
1475        List<String> allRightIds = new ArrayList<>();
1476        List<Map<String, Object>> noRightPages = new ArrayList<>();
1477        List<Map<String, Object>> errorPages = new ArrayList<>();
1478        List<Map<String, Object>> noModifiablePages = new ArrayList<>();
1479        
1480        for (String pageId : pageIds)
1481        {
1482            Page page = _resolver.resolveById(pageId);
1483            
1484            try
1485            {
1486                if (_rightManager.currentUserHasRight("Web_Rights_Page_BlankPage", page) != RightResult.RIGHT_ALLOW)
1487                {
1488                    noRightPages.add(Map.of("id", pageId, "title", page.getTitle()));
1489                }
1490                else if (!(page instanceof ModifiablePage))
1491                {
1492                    noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle()));
1493                }
1494                else
1495                {
1496                    ModifiablePage mPage = (ModifiablePage) page;
1497                    
1498                    if (page.getType().equals(PageType.CONTAINER))
1499                    {
1500                        // Remove zones
1501                        for (ModifiableZone zone : mPage.getZones())
1502                        {
1503                            zone.remove();
1504                        }
1505                    }
1506                    
1507                    mPage.setType(PageType.NODE);
1508                    mPage.getSitemap().saveChanges();
1509                    
1510                    allRightIds.add(pageId);
1511                    
1512                    Map<String, Object> eventParams = new HashMap<>();
1513                    eventParams.put(ObservationConstants.ARGS_PAGE, page);
1514                    _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
1515                }
1516            }
1517            catch (Exception e)
1518            {
1519                getLogger().error("Cannot set the page '" + pageId + "' as blank page", e);
1520                errorPages.add(Map.of("id", pageId, "title", page.getTitle()));
1521            }
1522        }
1523
1524        return Map.of("allright-pages", allRightIds, "noright-pages", noRightPages, "error-pages", errorPages, "nomodifiable-pages", noModifiablePages);
1525    }
1526    
1527    /**
1528     * Set a template to pages
1529     * @param pageIds the id of pages to update
1530     * @param templateName The template name
1531     * @return the id of pages which succeeded
1532     */
1533    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1534    public Map<String, Object> setTemplate (List<String> pageIds, String templateName)
1535    {
1536        return setTemplate(pageIds, templateName, true);
1537    }
1538    
1539    /**
1540     * Set a template to pages
1541     * @param pageIds the id of pages to update
1542     * @param templateName The template name
1543     * @param checkAvailableTemplate true if you want to check available template
1544     * @return the id of pages which succeeded
1545     */
1546    public Map<String, Object> setTemplate (List<String> pageIds, String templateName, boolean checkAvailableTemplate)
1547    {
1548        List<String> allRightIds = new ArrayList<>();
1549        List<Map<String, Object>> noRightPages = new ArrayList<>();
1550        List<Map<String, Object>> errorPages = new ArrayList<>();
1551        List<Map<String, Object>> noModifiablePages = new ArrayList<>();
1552        
1553        String defaultZoneName = null;
1554        
1555        for (String pageId : pageIds)
1556        {
1557            Page page = _resolver.resolveById(pageId);
1558            
1559            if (_rightManager.currentUserHasRight("Web_Rights_Page_Templates", page) != RightResult.RIGHT_ALLOW)
1560            {
1561                noRightPages.add(Map.of("id", pageId, "title", page.getTitle()));
1562            }
1563            else if (!(page instanceof ModifiablePage))
1564            {
1565                noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle()));
1566            }
1567            else
1568            {
1569                ModifiablePage mPage = (ModifiablePage) page;
1570                
1571                if (defaultZoneName == null)
1572                {
1573                    String skinId = page.getSite().getSkinId();
1574                    SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(templateName);
1575                    if (tpl == null)
1576                    {
1577                        throw new IllegalStateException("Template '" + templateName + "' does not exist on skin '" + skinId + "'");
1578                    }
1579                    
1580                    defaultZoneName = tpl.getDefaultZoneId();
1581                }
1582                
1583                if (checkAvailableTemplate && !_templatesHandler.getAvailablesTemplates(mPage).contains(templateName))
1584                {
1585                    throw new IllegalStateException("Template '" + templateName + "' is not available for page '" + pageId + "'");
1586                }
1587                
1588                if (page.getType().equals(PageType.CONTAINER))
1589                {
1590                    _removeOldZones (mPage, templateName);
1591                }
1592                
1593                mPage.setTemplate(templateName);
1594                mPage.setType(PageType.CONTAINER);
1595                mPage.getSitemap().saveChanges();
1596                
1597                allRightIds.add(pageId);
1598                
1599                Map<String, Object> eventParams = new HashMap<>();
1600                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1601                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
1602            }
1603            
1604        }
1605        
1606        Map<String, Object> results = new HashMap<>();
1607        results.put("allright-pages", allRightIds);
1608        results.put("noright-pages", noRightPages);
1609        results.put("error-pages", errorPages);
1610        results.put("nomodifiable-pages", noModifiablePages);
1611        
1612        if (defaultZoneName != null)
1613        {
1614            results.put("zonename", defaultZoneName);
1615        }
1616        return results;
1617    }
1618    
1619    /**
1620     * Get the script class name to execute to add or update this service
1621     * @param serviceId the service id
1622     * @return the script class name. Can be empty
1623     */
1624    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1625    public String getServiceParametersAction (String serviceId)
1626    {
1627        try
1628        {
1629            Service service = _serviceExtensionPoint.getExtension(serviceId);
1630            return service.getParametersScript().getScriptClassname();
1631        }
1632        catch (IllegalArgumentException e)
1633        {
1634            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
1635        }
1636    }
1637    
1638    private void _removeOldZones (ModifiablePage page, String templateName)
1639    {
1640        String skinId = page.getSite().getSkinId();
1641        
1642        SkinTemplate oldTemplate = _skinsManager.getSkin(skinId).getTemplate(templateName);
1643        
1644        Map<String, SkinTemplateZone> templateZones = oldTemplate.getZones();
1645        
1646        for (ModifiableZone zone : page.getZones())
1647        {
1648            if (!templateZones.containsKey(zone.getName()))
1649            {
1650                zone.remove();
1651            }
1652        }
1653    }
1654    
1655    /**
1656     * Get the tags from the pages
1657     * @param pageIds The ids of the pages
1658     * @return the tags of the pages
1659     */
1660    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
1661    public Set<String> getTags (List<String> pageIds)
1662    {
1663        Set<String> tags = new HashSet<>();
1664        
1665        for (String pageId : pageIds)
1666        {
1667            Page page = _resolver.resolveById(pageId);
1668            if (_rightManager.currentUserHasReadAccess(page))
1669            {
1670                tags.addAll(page.getTags());
1671            }
1672        }
1673        
1674        return tags;
1675    }
1676    
1677    /**
1678     * Tag a list of pages
1679     * @param pageIds The ids of pages to tag
1680     * @param tagNames The tags
1681     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1682     * @param contextualParameters Contextual parameters. Must contain the site name
1683     * @return the result
1684     */
1685    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
1686    public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1687    {
1688        return tag(pageIds, tagNames, TagMode.valueOf(mode), contextualParameters, false);
1689    }
1690    
1691    /**
1692     * Tag a list of pages
1693     * @param pageIds The ids of pages to tag
1694     * @param tagNames The tags
1695     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1696     * @param contextualParameters Contextual parameters. Must contain the site name
1697     * @param ignoreRights <code>true</code> to ignore rights on tag
1698     * @return the result
1699     */
1700    public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters, boolean ignoreRights)
1701    {
1702        Map<String, Object> result = new HashMap<>();
1703        
1704        result.put("nomodifiable-pages", new ArrayList<>());
1705        result.put("invalid-tags", new ArrayList<>());
1706        result.put("allright-pages", new ArrayList<>());
1707        result.put("noright-pages", new ArrayList<>());
1708        
1709        for (String pageId : pageIds)
1710        {
1711            Page page = _resolver.resolveById(pageId);
1712
1713            Map<String, Object> page2json = new HashMap<>();
1714            page2json.put("id", page.getId());
1715            page2json.put("title", page.getTitle());
1716            
1717            if (!ignoreRights && !_hasTagRights(page, tagNames, contextualParameters))
1718            {
1719                @SuppressWarnings("unchecked")
1720                List<Map<String, Object>> noRightPages = (List<Map<String, Object>>) result.get("noright-pages");
1721                noRightPages.add(page2json);
1722            }
1723            else
1724            {
1725                if (page instanceof ModifiablePage mPage)
1726                {
1727                    @SuppressWarnings("unchecked")
1728                    List<String> invalidTags = (List<String>) result.get("invalid-tags");
1729                    invalidTags.addAll(tag(mPage, tagNames, mode));
1730                    
1731                    page2json.put("tags", page.getTags());
1732                    @SuppressWarnings("unchecked")
1733                    List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages");
1734                    allRightPages.add(page2json);
1735                }
1736                else
1737                {
1738                    @SuppressWarnings("unchecked")
1739                    List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
1740                    nomodifiablePages.add(page2json);
1741                }
1742            }
1743        }
1744        
1745        return result;
1746    }
1747    
1748    private boolean _hasTagRights(Page page, List<String> tagNames, Map<String, Object> contextualParameters)
1749    {
1750        List<CMSTag> tags = tagNames.stream()
1751            .map(t -> _tagProvider.getTag(t, contextualParameters))
1752            .toList();
1753
1754        boolean hasPublicTagRight = _rightManager.currentUserHasRight("Web_Rights_Page_Tag", page) == RightResult.RIGHT_ALLOW;
1755        boolean hasPrivateTagRight = _rightManager.currentUserHasRight("Web_Rights_Page_Private_Tag", page) == RightResult.RIGHT_ALLOW;
1756        
1757        // Test if the current user has the right to tag public tag on page only if there are at least one public tag
1758        boolean hasRight = TagHelper.filterTags(tags, TagVisibility.PUBLIC, "PAGE").isEmpty() || hasPublicTagRight || hasPrivateTagRight;
1759        
1760        // Test if the current user has the right to tag private tag on page only if there are at least one private tag
1761        return hasRight && (TagHelper.filterTags(tags, TagVisibility.PRIVATE, "PAGE").isEmpty() || hasPrivateTagRight);
1762    }
1763
1764    /**
1765     * Tag a page
1766     * @param page The page to tag
1767     * @param tagNames The tags
1768     * @param tagMode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1769     * @return the list of invalid tags that were not set
1770     */
1771    public List<String> tag(ModifiablePage page, List<String> tagNames, TagMode tagMode)
1772    {
1773        List<String> invalidTags = new ArrayList<>();
1774        
1775        Set<String> oldTags = page.getTags();
1776        if (TagMode.REPLACE.equals(tagMode))
1777        {
1778            // First delete old tags
1779            for (String tagName : oldTags)
1780            {
1781                page.untag(tagName);
1782            }
1783        }
1784        
1785        // Then set new tags
1786        for (String tagName : tagNames)
1787        {
1788            if (_isTagValid(page, tagName))
1789            {
1790                if (TagMode.REMOVE.equals(tagMode))
1791                {
1792                    page.untag(tagName);
1793                }
1794                else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
1795                {
1796                    page.tag(tagName);
1797                }
1798            }
1799            else
1800            {
1801                invalidTags.add(tagName);
1802            }
1803        }
1804        
1805        page.saveChanges();
1806        
1807        if (!oldTags.equals(page.getTags()))
1808        {
1809            // Notify observers that the page has been tagged
1810            Map<String, Object> eventParams = new HashMap<>();
1811            eventParams.put(ObservationConstants.ARGS_PAGE, page);
1812            eventParams.put(ObservationConstants.ARGS_PAGE_TAGS, page.getTags());
1813            eventParams.put(ObservationConstants.ARGS_PAGE_OLD_TAGS, oldTags);
1814            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1815        }
1816        
1817        return invalidTags;
1818    }
1819    
1820    /**
1821     * Tag a list of contents with the given tags
1822     * @param pageIds The ids of pages to tag
1823     * @param contentIds The ids of contents to tag
1824     * @param tagNames The tags
1825     * @param contextualParameters The contextual parameters
1826     * @return the result map
1827     */
1828    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
1829    public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters)
1830    {
1831        return tag(pageIds, contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
1832    }
1833    
1834    /**
1835     * Tag a list of contents and/org pages
1836     * @param pageIds The ids of pages to tag
1837     * @param contentIds The ids of contents to tag
1838     * @param tagNames The tags
1839     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1840     * @param contextualParameters The contextual parameters
1841     * @return the result
1842     */
1843    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
1844    public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1845    {
1846        TagMode tagMode = TagMode.valueOf(mode);
1847        
1848        // Tag pages
1849        Map<String, Object> result = tag(pageIds, tagNames, tagMode, contextualParameters, false);
1850        
1851        // Tag contents
1852        result.putAll(_contentDAO.tag(contentIds, tagNames, tagMode, contextualParameters, false));
1853        
1854        // Invalid tags are ignored
1855        result.remove("invalid-tags");
1856        return result;
1857    }
1858    
1859    /**
1860     * Test if a tag is valid for a specific page
1861     * @param page The page
1862     * @param tagName The tag name
1863     * @return True if the tag is valid
1864     */
1865    public boolean _isTagValid (Page page, String tagName)
1866    {
1867        Map<String, Object> params = new HashMap<>();
1868        params.put("siteName", page.getSiteName());
1869        CMSTag tag = _tagProvider.getTag(tagName, params);
1870        
1871        return tag.getTarget().getName().equals("PAGE");
1872    }
1873    
1874    /**
1875     * Returns the ids of the pages matching the tag
1876     * @param sitename The site id
1877     * @param lang The language code
1878     * @param tag The tag id
1879     * @return Array of pages ids
1880     */
1881    public List<String> findPagedIdsByTag(String sitename, String lang, String tag)
1882    {
1883        return _getMemoryPageTagCache().get(PageTagCacheKey.of(sitename, lang, tag), __ -> _computePagesIds(sitename, lang, tag))
1884                    .stream()
1885                    .filter(_resolver::hasAmetysObjectForId) // Cache remember for tag in default, we have to check if page exists in current workspace
1886                    .collect(Collectors.toList());
1887    }
1888    
1889    private List<String> _computePagesIds(String sitename, String lang, String tag)
1890    {
1891        
1892        Session defaultSession = null;
1893        try
1894        {
1895            List<String> pagesIds = new ArrayList<>();
1896            
1897            // Force default workspace to execute query
1898            defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE);
1899            
1900            String xpath = PageQueryHelper.getPageXPathQuery(sitename, lang, null, new TagExpression(Operator.EQ, tag), null);
1901            AmetysObjectIterable<Page> pages = _ametysObjectResolver.query(xpath, defaultSession);
1902            Iterator<Page> it = pages.iterator();
1903    
1904            while (it.hasNext())
1905            {
1906                pagesIds.add(it.next().getId());
1907            }
1908            
1909            return pagesIds;
1910        }
1911        catch (RepositoryException e)
1912        {
1913            throw new AmetysRepositoryException(e);
1914        }
1915        finally
1916        {
1917            if (defaultSession != null)
1918            {
1919                defaultSession.logout();
1920            }
1921        }
1922    }
1923
1924    private Cache<PageTagCacheKey, List<String>> _getMemoryPageTagCache()
1925    {
1926        return _cacheManager.get(MEMORY_PAGESTAGCACHE);
1927    }
1928    
1929    /**
1930     * Set the visible of pages
1931     * @param pageIds The id of pages
1932     * @param visible <code>true</code> to set pages as visible, <code>false</code> otherwise
1933     * @return The result map
1934     */
1935    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1936    public Map<String, Object> setVisibility (List<String> pageIds, boolean visible)
1937    {
1938        Map<String, Object> result = new HashMap<>();
1939        
1940        result.put("nomodifiable-pages", new ArrayList<>());
1941        result.put("noright-pages", new ArrayList<>());
1942        result.put("allright-pages", new ArrayList<>());
1943        
1944        for (String id : pageIds)
1945        {
1946            Page page = _resolver.resolveById(id);
1947            
1948            Map<String, Object> page2json = new HashMap<>();
1949            page2json.put("id", page.getId());
1950            page2json.put("title", page.getTitle());
1951            
1952            if (_rightManager.currentUserHasRight("Web_Right_Page_Visibility", page) != RightResult.RIGHT_ALLOW)
1953            {
1954                @SuppressWarnings("unchecked")
1955                List<Map<String, Object>> norightPages = (List<Map<String, Object>>) result.get("noright-pages");
1956                norightPages.add(page2json);
1957            }
1958            else if (page instanceof ModifiablePage)
1959            {
1960                ModifiablePage mPage = (ModifiablePage) page;
1961                mPage.setVisible(visible);
1962                mPage.saveChanges();
1963                
1964                Map<String, Object> eventParams = new HashMap<>();
1965                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1966                eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1967                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1968                
1969                @SuppressWarnings("unchecked")
1970                List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages");
1971                allRightPages.add(page2json);
1972            }
1973            else
1974            {
1975                @SuppressWarnings("unchecked")
1976                List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
1977                nomodifiablePages.add(page2json);
1978            }
1979        }
1980        
1981        return result;
1982    }
1983    
1984    /**
1985     * Get the contents of a {@link Page} and its subpages which can be deleted.
1986     * A content is deleteable if user has right, the content is not locked and not referenced by other pages.
1987     * @param id The id of page
1988     * @return The list of deletable contents
1989     */
1990    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
1991    public Map<String, Object> getDeleteablePageContents (String id)
1992    {
1993        Map<String, Object> results = new HashMap<>();
1994        
1995        results.put("deleteable-contents", new ArrayList<>());
1996        results.put("referenced-contents", new ArrayList<>());
1997        results.put("unauthorized-contents", new ArrayList<>());
1998        results.put("locked-contents", new ArrayList<>());
1999        
2000        Page page = _resolver.resolveById(id);
2001        List<Content> contents = getPageContents(page, true);
2002        
2003        for (Content content : contents)
2004        {
2005            Map<String, Object> contentParams = new HashMap<>();
2006            contentParams.put("id", content.getId());
2007            contentParams.put("title", content.getTitle(new Locale(page.getSitemapName())));
2008            contentParams.put("name", content.getName());
2009            
2010            if (_isReferenced(content))
2011            {
2012                // Content is referenced by at least another page
2013                @SuppressWarnings("unchecked")
2014                List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) results.get("referenced-contents");
2015                referencedContents.add(contentParams);
2016            }
2017            else if (!_contentDAO.canDelete(content))
2018            {
2019                @SuppressWarnings("unchecked")
2020                List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) results.get("unauthorized-contents");
2021                unauthorizedContents.add(contentParams);
2022            }
2023            else if (_isLocked(content))
2024            {
2025                // If the content is locked by other
2026                @SuppressWarnings("unchecked")
2027                List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
2028                lockedContents.add(contentParams);
2029            }
2030            else
2031            {
2032                Map<String, Object> content2json = new HashMap<>();
2033                content2json.put("id", content.getId());
2034                content2json.put("name", content.getName());
2035                content2json.put("title", content.getTitle(new Locale(page.getSitemapName())));
2036                content2json.put("isNew", _isNew(content));
2037                content2json.put("isShared", content instanceof SharedContent);
2038                content2json.put("hasShared", _sharedContentManager.hasSharedContents(content));
2039                
2040                @SuppressWarnings("unchecked")
2041                List<Map<String, Object>> allrightContents = (List<Map<String, Object>>) results.get("deleteable-contents");
2042                allrightContents.add(content2json);
2043            }
2044        }
2045        
2046        return results;
2047    }
2048    
2049    /**
2050     * Get the contents that belong to the {@link Page} and its sub-pages and that can be deleted.
2051     * A content is deleteable if user has right, the content is not locked and it's not referenced by other pages.
2052     * If 'onlyNewlyCreatedContents' is set to 'true', only newly created contents will be returned
2053     * @param pageId The id of page
2054     * @param onlyNewlyCreatedContents true to return only the newly created contents
2055     * @return The ids of deleteable contents
2056     */
2057    public List<String> getDeleteablePageContentIds (String pageId, boolean onlyNewlyCreatedContents)
2058    {
2059        List<String> contentsId = new ArrayList<>();
2060        
2061        Page page = _resolver.resolveById(pageId);
2062        
2063        List<Content> contents = getPageContents(page, true);
2064        
2065        for (Content content : contents)
2066        {
2067            if (_contentDAO.canDelete(content) && !_isLocked(content) && !_isReferenced(content))
2068            {
2069                if (!onlyNewlyCreatedContents || _isNew(content))
2070                {
2071                    contentsId.add(content.getId());
2072                }
2073            }
2074        }
2075        
2076        return contentsId;
2077    }
2078        
2079    /**
2080     * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem}
2081     * @param id The id of page or zone item
2082     * @return The list of unreferenced contents
2083     */
2084    @Callable (rights = Callable.READ_ACCESS, rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
2085    public List<Map<String, Object>> getUnreferencedContents (String id)
2086    {
2087        List<Map<String, Object>> unreferencedContents = new ArrayList<>();
2088        
2089        Page page = _resolver.resolveById(id);
2090        List<Content> contents = getPageContents(page, true);
2091        
2092        for (Content content : contents)
2093        {
2094            if (!_isReferenced(content))
2095            {
2096                Map<String, Object> content2json = new HashMap<>();
2097                content2json.put("id", content.getId());
2098                content2json.put("name", content.getName());
2099                content2json.put("title", content.getTitle(new Locale(page.getSitemapName())));
2100                content2json.put("isNew", _isNew(content));
2101                content2json.put("isShared", content instanceof SharedContent);
2102                content2json.put("hasShared", _sharedContentManager.hasSharedContents(content));
2103                
2104                unreferencedContents.add(content2json);
2105            }
2106        }
2107        
2108        return unreferencedContents;
2109    }
2110    
2111    /**
2112     * Returns the page's attachments root node
2113     * @param id the page's id
2114     * @return The attachments' root node informations
2115     */
2116    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
2117    public Map<String, Object> getAttachmentsRootNode (String id)
2118    {
2119        Map<String, Object> result = new HashMap<>();
2120        
2121        Page page = _resolver.resolveById(id);
2122        
2123        result.put("title", page.getTitle());
2124        result.put("pageId", page.getId());
2125        
2126        if (_rightManager.currentUserHasRight("Web_Rights_Page_Attachments", page) != RightResult.RIGHT_ALLOW
2127            && _rightManager.currentUserHasRight("Web_Rights_Page_AttachmentHandle", page) != RightResult.RIGHT_ALLOW)
2128        {
2129            return result;
2130        }
2131        
2132        TraversableAmetysObject attachments = page.getRootAttachments();
2133        
2134        if (attachments != null)
2135        {
2136            result.put("id", attachments.getId());
2137            if (attachments instanceof ModifiableAmetysObject)
2138            {
2139                result.put("isModifiable", true);
2140            }
2141            if (attachments instanceof ModifiableResourceCollection)
2142            {
2143                result.put("canCreateChild", true);
2144            }
2145            
2146            boolean hasChildNodes = false;
2147            boolean hasResources = false;
2148
2149            for (AmetysObject child : attachments.getChildren())
2150            {
2151                if (child instanceof Resource)
2152                {
2153                    hasResources = true;
2154                }
2155                else if (child instanceof ExplorerNode)
2156                {
2157                    hasChildNodes = true;
2158                }
2159            }
2160
2161            if (hasChildNodes)
2162            {
2163                result.put("hasChildNodes", true);
2164            }
2165
2166            if (hasResources)
2167            {
2168                result.put("hasResources", true);
2169            }
2170            
2171            return result;
2172        }
2173        
2174        throw new IllegalArgumentException("Page with id '" + id + "' does not support attachments.");
2175    }
2176    /**
2177     * Returns the page's parents ids
2178     * @param id the page's id
2179     * @return The attachments' root node informations
2180     */
2181    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
2182    public Map<String, Object> getPageParents (String id)
2183    {
2184        Map<String, Object> result = new HashMap<>();
2185        List<Map<String, Object>> pages = new ArrayList<>();
2186        Page page = _resolver.resolveById(id);
2187        pages.add(_page2Json(page));
2188        while (page.getParent() != null && page.getParent() instanceof Page)
2189        {
2190            page = page.getParent();
2191            pages.add(_page2Json(page));
2192        }
2193        result.put("parents", pages);
2194        return result;
2195    }
2196    
2197    /**
2198     * Get the contents of a page and its child pages
2199     * @param sitemapElement The page
2200     * @return The list of contents
2201     */
2202    public List<Content> getPageContents (SitemapElement sitemapElement)
2203    {
2204        return getPageContents(sitemapElement, false);
2205    }
2206    
2207    /**
2208     * Get the contents of a page and its child pages
2209     * @param sitemapElement The page
2210     * @param ignoreContentsOfNonRemovablePage true to ignore contents of non-removable pages (virtual pages)
2211     * @return The list of contents
2212     */
2213    public List<Content> getPageContents (SitemapElement sitemapElement, boolean ignoreContentsOfNonRemovablePage)
2214    {
2215        List<Content> contents = new ArrayList<>();
2216        
2217        if ((!ignoreContentsOfNonRemovablePage || sitemapElement instanceof RemovableAmetysObject)
2218                && sitemapElement.getTemplate() != null)
2219        {
2220            for (Zone zone : sitemapElement.getZones())
2221            {
2222                try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems())
2223                {
2224                    for (ZoneItem zoneItem : zoneItems)
2225                    {
2226                        if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT)
2227                        {
2228                            contents.add(zoneItem.getContent());
2229                        }
2230                    }
2231                }
2232            }
2233        }
2234        
2235        AmetysObjectIterable< ? extends Page> childrenPages = sitemapElement.getChildrenPages();
2236        for (Page childPage : childrenPages)
2237        {
2238            contents.addAll(getPageContents(childPage, ignoreContentsOfNonRemovablePage));
2239        }
2240        
2241        return contents;
2242    }
2243    
2244    /**
2245     * Get the user rights on sitemap element (page or sitemap)
2246     * @param pagesCt The sitemap element
2247     * @return The user's rights
2248     */
2249    protected Set<String> getUserRights (SitemapElement pagesCt)
2250    {
2251        UserIdentity user = _currentUserProvider.getUser();
2252        
2253        Set<String> userRights = _rightManager.getUserRights(user, pagesCt);
2254        
2255        // Do some specific stuff here, because the right 'Web_Rights_Page_Delete' is a right to delete child pages and not the page itself.
2256        // So the right should be checked on parent context.
2257        if (pagesCt instanceof Page)
2258        {
2259            SitemapElement parent = pagesCt.getParent();
2260            boolean canDelete = _rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW;
2261            if (!canDelete)
2262            {
2263                // No right on parent page, so remove the right if exists.
2264                userRights.remove("Web_Rights_Page_Delete");
2265            }
2266        }
2267        else
2268        {
2269            // There is no right of deletion on the sitemap
2270            userRights.remove("Web_Rights_Page_Delete");
2271        }
2272        
2273        return userRights;
2274    }
2275    
2276    private boolean _isReferenced (Content content)
2277    {
2278        return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1;
2279    }
2280    
2281    private boolean _isLocked (Content content)
2282    {
2283        if (content instanceof LockableAmetysObject)
2284        {
2285            LockableAmetysObject lockableContent = (LockableAmetysObject) content;
2286            if (lockableContent.isLocked())
2287            {
2288                boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW;
2289                if (!LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) && !canUnlockAll)
2290                {
2291                    return true;
2292                }
2293            }
2294        }
2295        
2296        return false;
2297    }
2298    
2299    private boolean _isNew (Content content)
2300    {
2301        boolean isNew = false;
2302        if (content instanceof WorkflowAwareContent)
2303        {
2304            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
2305            long workflowId = waContent.getWorkflowId();
2306            
2307            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
2308            isNew = workflow.getHistorySteps(workflowId).isEmpty();
2309        }
2310        return isNew;
2311    }
2312
2313    private Map<String, Object> _page2Json (Page page)
2314    {
2315        Map<String, Object> page2json = new HashMap<>();
2316        page2json.put("id", page.getId());
2317        page2json.put("title", page.getTitle());
2318        page2json.put("siteName", page.getSiteName());
2319        page2json.put("path", page.getPathInSitemap());
2320        return page2json;
2321    }
2322    
2323    private Map<String, Object> _content2Json (Content content, Locale locale)
2324    {
2325        Map<String, Object> content2json = new HashMap<>();
2326        content2json.put("id", content.getId());
2327        content2json.put("title", content.getTitle(locale));
2328        content2json.put("name", content.getName());
2329        
2330        List<Map<String, Object>> pages = new ArrayList<>();
2331        if (content instanceof WebContent)
2332        {
2333            content2json.put("siteName", ((WebContent) content).getSiteName());
2334            Collection<Page> refPages = ((WebContent) content).getReferencingPages();
2335            for (Page refPage : refPages)
2336            {
2337                pages.add(_page2Json(refPage));
2338            }
2339            content2json.put("pages", pages);
2340        }
2341        
2342        return content2json;
2343    }
2344    
2345    private Map<String, Object> _publication2Json (Page page)
2346    {
2347        Map<String, Object> pub2json = new HashMap<>();
2348        
2349        if (page.hasValue(DefaultPage.METADATA_PUBLICATION_START_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID))
2350        {
2351            Object startDateAsJSON = page.dataToJSON(DefaultPage.METADATA_PUBLICATION_START_DATE);
2352            pub2json.put("startDate", startDateAsJSON);
2353        }
2354        
2355        if (page.hasValue(DefaultPage.METADATA_PUBLICATION_END_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID))
2356        {
2357            Object endDateAsJSON = page.dataToJSON(DefaultPage.METADATA_PUBLICATION_END_DATE);
2358            pub2json.put("endDate", endDateAsJSON);
2359        }
2360        
2361        return pub2json;
2362        
2363    }
2364    
2365    private AmetysObjectIterable<Content> _getIncomingContentReferences (String pageId)
2366    {
2367        String xpathQuery = "//element(*, ametys:content)[ametys-internal:consistency/@ametys-internal:link = 'page:" + pageId + "']";
2368        return _resolver.query(xpathQuery);
2369    }
2370    
2371    private AmetysObjectIterable<Page> _getIncomingPageReferences (String pageId)
2372    {
2373        String xpathQuery = "//element(*, ametys:page)[@ametys-internal:type = 'LINK' and @ametys-internal:url= '" + pageId + "']";
2374        return _resolver.query(xpathQuery);
2375    }
2376    
2377    private void _updateContentsAfterCopy(Page initialPage, Page createdPage) throws AmetysRepositoryException
2378    {
2379        for (Zone zone : createdPage.getZones())
2380        {
2381            Zone initialZone = initialPage.getZone(zone.getName());
2382            try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems(); AmetysObjectIterable< ? extends ZoneItem> initialZoneItems = initialZone.getZoneItems())
2383            {
2384                AmetysObjectIterator<? extends ZoneItem> iterator = zoneItems.iterator();
2385                AmetysObjectIterator< ? extends ZoneItem> initialIterator = initialZoneItems.iterator();
2386                if (iterator.getSize() != initialIterator.getSize())
2387                {
2388                    throw new IllegalStateException("An error occured during the copy of " + initialPage.getName() + " (" + initialPage.getId() + "). The resulting page have a different number of zoneItems.");
2389                }
2390                while (iterator.hasNext())
2391                {
2392                    ZoneItem zoneItem = iterator.next();
2393                    ZoneItem initialZoneItem = initialIterator.next();
2394                    if (zoneItem.getType().equals(ZoneType.CONTENT))
2395                    {
2396                        Content content = zoneItem.getContent();
2397                        WebContent initialContent = initialZoneItem.getContent();
2398                        
2399                        // Updaters
2400                        Set<String> ids = _copyUpdaterEP.getExtensionsIds();
2401                        for (String id : ids)
2402                        {
2403                            _copyUpdaterEP.getExtension(id).updateContent(initialPage.getSite(), createdPage.getSite(), initialContent, content);
2404                        }
2405                        
2406                        // Convert content language if necessary
2407                        if (content instanceof ModifiableContent && content.getLanguage() != null && content.getLanguage() != createdPage.getSitemapName())
2408                        {
2409                            ((ModifiableContent) content).setLanguage(createdPage.getSitemapName());
2410                            ((ModifiableContent) content).saveChanges();
2411                        }
2412                        
2413                        // Create the first version
2414                        if (content instanceof VersionableAmetysObject)
2415                        {
2416                            ((VersionableAmetysObject) content).checkpoint();
2417                        }
2418                    }
2419                }
2420            }
2421        }
2422        
2423        // Browse child pages
2424        for (Page childPage : createdPage.getChildrenPages())
2425        {
2426            Page initialChildPage = initialPage.getChild(childPage.getName());
2427            _updateContentsAfterCopy (initialChildPage, childPage);
2428        }
2429    }
2430    
2431    private List<String> _getChildrenPageIds (Page page)
2432    {
2433        List<String> childIds = new ArrayList<>();
2434        
2435        for (Page childPage : page.getChildrenPages())
2436        {
2437            childIds.add(childPage.getId());
2438            childIds.addAll(_getChildrenPageIds(childPage));
2439        }
2440        
2441        return childIds;
2442    }
2443
2444    /**
2445     * Check each parent and return true if one of them is invisible
2446     * @param page page to check
2447     * @return true if at least one parent is invisible
2448     */
2449    private boolean _isParentInvisible (Page page)
2450    {
2451        AmetysObject parent = page.getParent();
2452        while (parent != null && parent instanceof Page)
2453        {
2454            boolean invisible = !((Page) parent).isVisible();
2455            if (invisible)
2456            {
2457                return true;
2458            }
2459            parent = parent.getParent();
2460        }
2461        return false;
2462    }
2463
2464    /* start of a group of methods for _isPreviewable */
2465    /**
2466     * Determine if this page is previewable
2467     * @param page The page to look at
2468     * @return true if the page can be previewed
2469     */
2470    private boolean _isPreviewable(Page page)
2471    {
2472        // Check for infinitive loop redirection
2473        ArrayList<String> pagesSequence = new ArrayList<>();
2474        pagesSequence.add(page.getId());
2475        if (_isInfiniteRedirection (page, pagesSequence))
2476        {
2477            getLogger().error("An infinite loop redirection was detected for page '" + page.getPathInSitemap() + "'");
2478            return false;
2479        }
2480
2481        if (page.getType() == PageType.LINK && LinkType.PAGE.equals(page.getURLType()))
2482        {
2483            return _isPageExist(page.getURL()) && _isPreviewable((Page) _resolver.resolveById(page.getURL()));
2484        }
2485
2486        if (page.getType() != PageType.NODE)
2487        {
2488            return true;
2489        }
2490        else
2491        {
2492            try (AmetysObjectIterable< ? extends Page> childrenPages = page.getChildrenPages())
2493            {
2494                for (Page subPage : childrenPages)
2495                {
2496                    if (_isPreviewable(subPage))
2497                    {
2498                        return true;
2499                    }
2500                }
2501            }
2502        }
2503        return false;
2504    }
2505
2506    private boolean _isPageExist (String id)
2507    {
2508        try
2509        {
2510            _resolver.resolveById(id);
2511            return true;
2512        }
2513        catch (UnknownAmetysObjectException e)
2514        {
2515            return false;
2516        }
2517    }
2518
2519    private boolean _isInfiniteRedirection (Page page, List<String> pagesSequence)
2520    {
2521        Page redirectPage = _getPageRedirection (page);
2522        if (redirectPage == null)
2523        {
2524            return false;
2525        }
2526
2527        if (pagesSequence.contains(redirectPage.getId()))
2528        {
2529            return true;
2530        }
2531
2532        pagesSequence.add(redirectPage.getId());
2533        return _isInfiniteRedirection (redirectPage, pagesSequence);
2534    }
2535
2536    private Page _getPageRedirection (Page page)
2537    {
2538        if (PageType.LINK.equals(page.getType()) && LinkType.PAGE.equals(page.getURLType()))
2539        {
2540            try
2541            {
2542                String pageId = page.getURL();
2543                return _resolver.resolveById(pageId);
2544            }
2545            catch (AmetysRepositoryException e)
2546            {
2547                return null;
2548            }
2549        }
2550        else if (PageType.NODE.equals(page.getType()))
2551        {
2552            AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages();
2553            Iterator<? extends Page> it = childPages.iterator();
2554            if (it.hasNext())
2555            {
2556                return it.next();
2557            }
2558        }
2559
2560        return null;
2561    }
2562    /* end of a group of methods for _isPreviewable */
2563    
2564    private static final class PageTagCacheKey extends AbstractCacheKey
2565    {
2566        private PageTagCacheKey(String sitename, String lang, String tag)
2567        {
2568            super(sitename, lang, tag);
2569        }
2570        
2571        private PageTagCacheKey(String sitename, String lang)
2572        {
2573            super(sitename, lang);
2574        }
2575
2576        static PageTagCacheKey of(String sitename, String lang)
2577        {
2578            return new PageTagCacheKey(sitename, lang, null);
2579        }
2580
2581        static PageTagCacheKey of(String sitename, String lang, String tag)
2582        {
2583            return new PageTagCacheKey(sitename, lang, tag);
2584        }
2585    }
2586}