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