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                // in case of reorder, check right of creation on current parent page
733                if (!_canCreate(srcParent))
734                {
735                    throw new IllegalStateException("You do not have to reorder a page under '/" + srcParent.getSitemapName() + "/" + srcParent.getPathInSitemap() + "'");
736                }
737                
738                Page brother = srcParent.getChildPageAt(index);
739                ((MoveablePage) page).orderBefore(brother);
740            }
741            catch (UnknownAmetysObjectException e)
742            {
743                // Move the last child position
744                ((MoveablePage) page).orderBefore(null);
745            }
746            
747            // Path is not modified
748            newPathInSitemap = oldPathInSitemap;
749        }
750        else
751        {
752            SitemapElement newParentPage = _resolver.resolveById(parentId);
753
754            // check right of creation on new parent page
755            if (!_canCreate(newParentPage))
756            {
757                throw new IllegalStateException("You do not have the rights to create a page under '/" + newParentPage.getSitemapName() + "/" + newParentPage.getPathInSitemap() + "'");
758            }
759
760            ((MoveablePage) page).moveTo(newParentPage, true);
761            if (index != -1)
762            {
763                Page brother = newParentPage.getChildPageAt(index);
764                
765                ((MoveablePage) page).orderBefore(brother);
766            }
767            
768            // Path is modified
769            newPathInSitemap = page.getPathInSitemap();
770        }
771        
772        if (sitemap.needsSave())
773        {
774            sitemap.saveChanges();
775        }
776
777        // Notify observers that the page has been moved
778        Map<String, Object> eventParams = new HashMap<>();
779        eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
780        eventParams.put(ObservationConstants.ARGS_PAGE, page);
781        eventParams.put("page.old.path", oldPathInSitemap);
782        eventParams.put("page.old.parent", srcParent);
783        eventParams.put(ObservationConstants.ARGS_PAGE_PATH, newPathInSitemap);
784        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_MOVED, _currentUserProvider.getUser(), eventParams));
785        
786        result.put("id", page.getId());
787        result.put("parentId", page.getParent().getId());
788        
789        return result;
790    }
791    
792    private boolean _canCreate(SitemapElement parentPage)
793    {
794        UserIdentity user = _currentUserProvider.getUser();
795        if (_rightManager.hasRight(user, "Web_Rights_Page_Create", parentPage) == RightResult.RIGHT_ALLOW)
796        {
797            return true;
798        }
799        
800        if (getLogger().isInfoEnabled())
801        {
802            getLogger().info("The user '" + user + "' tried to create page under '/" + parentPage.getSitemapName() + "/" + parentPage.getPathInSitemap() + "' without sufficient rights");
803        }
804        
805        return false;
806    }
807    
808    private boolean _canDelete(Page page)
809    {
810        UserIdentity user = _currentUserProvider.getUser();
811        SitemapElement parent = page.getParent();
812        if (_rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW)
813        {
814            return true;
815        }
816        
817        if (getLogger().isInfoEnabled())
818        {
819            getLogger().info("The user '" + user + "' tried to move page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' without sufficient rights");
820        }
821        
822        return false;
823    }
824    
825    /**
826     * Set pages as redirection
827     * @param pageIds the id of pages to modify
828     * @param url the url of redirection
829     * @param urlType the type of redirection
830     * @return the id of pages which succeeded or failed. 
831     */
832    @Callable
833    public Map<String, Object> setLink (List<String> pageIds, String url, String urlType)
834    {
835        Map<String, Object> result = new HashMap<>();
836        
837        if (StringUtils.isEmpty(url))
838        {
839            throw new IllegalArgumentException("Can not set page as a redirection with an empty url");
840        }
841        
842        List<String> successes = new ArrayList<>();
843        List<Map<String, Object>> failures = new ArrayList<>();
844        
845        for (String pageId : pageIds)
846        {
847            try
848            {
849                Page page = _resolver.resolveById(pageId);
850                if (!(page instanceof ModifiablePage))
851                {
852                    throw new IllegalArgumentException("Can not set page as a redirection on a non-modifiable page " + pageId);
853                }
854                
855                ModifiablePage mPage = (ModifiablePage) page;
856                
857                if (page.getType().equals(PageType.CONTAINER))
858                {
859                    // Remove zones
860                    for (ModifiableZone zone : mPage.getZones())
861                    {
862                        zone.remove();
863                    }
864                }
865                
866                if (pageId.equals(url))
867                {
868                    throw new IllegalArgumentException("A page can not redirect to itself");
869                }
870                
871                mPage.setType(PageType.LINK);
872                mPage.setURL(LinkType.valueOf(urlType), url);
873                mPage.getSitemap().saveChanges();
874                
875                successes.add(pageId);
876                
877                Map<String, Object> eventParams = new HashMap<>();
878                eventParams.put(ObservationConstants.ARGS_PAGE, page);
879                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
880            }
881            catch (Exception e)
882            {
883                getLogger().error("Cannot set the page '" + pageId + "' as link [" + url + ", " + urlType.toString() + "]", e);
884                
885                Map<String, Object> failure = new HashMap<>();
886                failure.put("id", pageId);
887                failure.put("error", e.toString());
888                failures.add(failure);
889            }
890        }
891
892        result.put("success", successes);
893        result.put("failure", failures);
894        
895        return result;
896    }
897    
898    /**
899     * Get available template for specified page
900     * @param pageId The page's id
901     * @return the list of available template
902     */
903    @Callable
904    public List<Map<String, Object>> getAvailableTemplates (String pageId)
905    {
906        List<Map<String, Object>> templates = new ArrayList<>();
907        
908        Page page = _resolver.resolveById(pageId);
909        
910        Set<String> availableTemplateIds = _templatesHandler.getAvailablesTemplates(page);
911        for (String templateName : availableTemplateIds)
912        {
913            String skinId = page.getSite().getSkinId();
914            Skin skin = _skinsManager.getSkin(skinId);
915            
916            SkinTemplate template = skin.getTemplate(templateName);
917            
918            Map<String, Object> template2json = new HashMap<>();
919            template2json.put("id", template.getId());
920            template2json.put("label", template.getLabel());
921            template2json.put("description", template.getDescription());
922            template2json.put("iconSmall", template.getSmallImage());
923            template2json.put("iconMedium", template.getMediumImage());
924            template2json.put("iconLarge", template.getLargeImage());
925            template2json.put("zone", template.getDefaultZoneId());
926            
927            templates.add(template2json);
928        }
929        
930        return templates;
931    }
932    
933    /**
934     * Get available content types for specified page
935     * @param pageId The page's id
936     * @param zoneName the name of the zone
937     * @return the list of available content types
938     */
939    @Callable
940    public List<Map<String, Object>> getAvailableContentTypes (String pageId, String zoneName)
941    {
942        List<Map<String, Object>> contenttypes = new ArrayList<>();
943        
944        Page page = _resolver.resolveById(pageId);
945        
946        Set<String> contentTypeIds = _cTypeHandler.getAvailableContentTypes(page, zoneName);
947        for (String contentTypeId : contentTypeIds)
948        {
949            ContentType cType = _contentTypeExtensionPoint.getExtension(contentTypeId);
950            
951            if (cType != null && _hasRight(cType, page))
952            {
953                Map<String, Object> ctype2json = new HashMap<>();
954                ctype2json.put("id", cType.getId());
955                ctype2json.put("label", cType.getLabel());
956                ctype2json.put("description", cType.getDescription());
957                ctype2json.put("iconGlyph", cType.getIconGlyph());
958                ctype2json.put("iconDecorator", cType.getIconDecorator());
959                ctype2json.put("iconSmall", cType.getSmallIcon());
960                ctype2json.put("iconMedium", cType.getMediumIcon());
961                ctype2json.put("iconLarge", cType.getLargeIcon());
962                ctype2json.put("defaultTitle", cType.getDefaultTitle());
963                ctype2json.put("viewNames", cType.getViewNames(true));
964                ctype2json.put("category", cType.getCategory().isI18n() ? cType.getCategory().getKey() : cType.getCategory().getLabel());
965                ctype2json.put("categoryLabel", cType.getCategory());
966                
967                contenttypes.add(ctype2json);
968            }
969        }
970        
971        return contenttypes;
972    }
973    
974    /**
975     * Get available content types for a page being created
976     * @param pageId The page's id. Can be null of the page is not yet created
977     * @param zoneName the name of the zone
978     * @param parentId The id of parent page
979     * @param pageTitle The title of page to create
980     * @param template The template of page to create
981     * @return the list of available services
982     */
983    @Callable
984    public List<Map<String, Object>> getAvailableContentTypesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template)
985    {
986        if (StringUtils.isNotEmpty(pageId))
987        {
988            // Get available services for a page
989            return getAvailableContentTypes(pageId, zoneName);
990        }
991        else if (StringUtils.isNotEmpty(parentId))
992        {
993            // Get available services for a not yet existing page
994            SitemapElement parent = _resolver.resolveById(parentId);
995            
996            // Create page temporarily
997            Page page = _createPage(parent, pageTitle, template);
998            
999            List<Map<String, Object>> availableContentTypes = getAvailableContentTypes(page.getId(), zoneName);
1000            
1001            // Cancel page creation
1002            page.getSitemap().revertChanges();
1003            
1004            return availableContentTypes;
1005        }
1006        
1007        return Collections.EMPTY_LIST;
1008    }
1009    
1010    private boolean _hasRight(ContentType contentType, Page page)
1011    {
1012        String right = contentType.getRight();
1013        
1014        if (right == null)
1015        {
1016            return true;
1017        }
1018        else
1019        {
1020            UserIdentity user = _currentUserProvider.getUser();
1021            return _rightManager.hasRight(user, right, page) == RightResult.RIGHT_ALLOW;
1022        }
1023    }
1024    
1025    /**
1026     * Get available services for specified page
1027     * @param pageId The page's id
1028     * @param zoneName the name of the zone
1029     * @return the list of available services
1030     */
1031    @Callable
1032    public List<Map<String, Object>> getAvailableServices (String pageId, String zoneName)
1033    {
1034        List<Map<String, Object>> services = new ArrayList<>();
1035        
1036        SitemapElement sitemapElement = _resolver.resolveById(pageId);
1037        
1038        Set<String> serviceIds = _serviceHandler.getAvailableServices(sitemapElement, zoneName);
1039        for (String serviceId : serviceIds)
1040        {
1041            Service service = _serviceExtensionPoint.getExtension(serviceId);
1042            if (service != null && _hasRight(service, sitemapElement))
1043            {
1044                Map<String, Object> serviceMap = new HashMap<>();
1045                serviceMap.put("id", service.getId());
1046                serviceMap.put("label", service.getLabel());
1047                serviceMap.put("description", service.getDescription());
1048                serviceMap.put("iconGlyph", service.getIconGlyph());
1049                serviceMap.put("iconDecorator", service.getIconDecorator());
1050                serviceMap.put("iconSmall", service.getSmallIcon());
1051                serviceMap.put("iconMedium", service.getMediumIcon());
1052                serviceMap.put("iconLarge", service.getLargeIcon());
1053                serviceMap.put("parametersAction", service.getParametersScript().getScriptClassname());
1054                serviceMap.put("category", service.getCategory().isI18n() ? service.getCategory().getKey() : service.getCategory().getLabel());
1055                serviceMap.put("categoryLabel", service.getCategory());
1056                
1057                services.add(serviceMap);
1058            }
1059        }
1060        
1061        return services;
1062    }
1063    
1064    /**
1065     * Get available services for a page being created
1066     * @param pageId The page's id. Can be null of the page is not yet created
1067     * @param zoneName the name of the zone
1068     * @param parentId The id of parent page
1069     * @param pageTitle The title of page to create
1070     * @param template The template of page to create
1071     * @return the list of available services
1072     */
1073    @Callable
1074    public List<Map<String, Object>> getAvailableServicesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template)
1075    {
1076        if (StringUtils.isNotEmpty(pageId))
1077        {
1078            // Get available services for a page
1079            return getAvailableServices(pageId, zoneName);
1080        }
1081        else if (StringUtils.isNotEmpty(parentId))
1082        {
1083            // Get available services for a not yet existing page
1084            SitemapElement parent = _resolver.resolveById(parentId);
1085            
1086            // Create page temporarily
1087            Page page = _createPage(parent, pageTitle, template);
1088            
1089            List<Map<String, Object>> availableServices = getAvailableServices(page.getId(), zoneName);
1090            
1091            // Cancel page creation
1092            page.getSitemap().revertChanges();
1093            
1094            return availableServices;
1095        }
1096        
1097        return Collections.EMPTY_LIST;
1098    }
1099    
1100    private Page _createPage (SitemapElement parent, String pageTitle, String template)
1101    {
1102        Site site = parent.getSite();
1103        String originalPageName = NameHelper.filterName(pageTitle);
1104        
1105        String pageName = originalPageName;
1106        int index = 2;
1107        while (parent.hasChild(pageName))
1108        {
1109            pageName = originalPageName + "-" + index++;
1110        }
1111        
1112        ModifiablePage page = ((ModifiableTraversableAmetysObject) parent).createChild(pageName, "ametys:defaultPage");
1113        
1114        page.setTitle(pageTitle);
1115        page.setType(PageType.NODE);
1116        page.setSiteName(site.getName());
1117        page.setSitemapName(page.getSitemap().getName());
1118
1119        if (template != null)
1120        {
1121            String skinId = page.getSite().getSkinId();
1122            SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(template);
1123            if (tpl == null)
1124            {
1125                throw new IllegalStateException("Template '" + template + "' does not exist on skin '" + skinId + "'");
1126            }
1127            
1128            // Set temporary the template to get available services
1129            page.setType(PageType.CONTAINER); 
1130            page.setTemplate(template);
1131        }
1132        
1133        return page;
1134    }
1135    
1136    private boolean _hasRight(Service service, SitemapElement sitemapElement)
1137    {
1138        String right = service.getRight();
1139        
1140        if (right == null)
1141        {
1142            return true;
1143        }
1144        else
1145        {
1146            UserIdentity user = _currentUserProvider.getUser();
1147            return _rightManager.hasRight(user, right, sitemapElement) == RightResult.RIGHT_ALLOW;
1148        }
1149    }
1150    
1151    /**
1152     * Get available template for specified pages
1153     * @param pageIds The id of pages
1154     * @return the list of available template
1155     */
1156    @Callable
1157    public List<Map<String, Object>> getAvailableTemplates (List<String> pageIds)
1158    {
1159        List<String> templateIds = new ArrayList<>();
1160        
1161        List<Map<String, Object>> templates = new ArrayList<>();
1162        
1163        for (String pageId : pageIds)
1164        {
1165            List<Map<String, Object>> pageTemplates = getAvailableTemplates(pageId);
1166            
1167            for (Map<String, Object> template : pageTemplates)
1168            {
1169                String templateName = (String) template.get("id");
1170                
1171                if (!templateIds.contains(templateName))
1172                {
1173                    templateIds.add(templateName);
1174                    templates.add(template);
1175                }
1176            }
1177        }
1178        
1179        return templates;
1180    }
1181    
1182    /**
1183     * Get available content types for a page being created
1184     * @param pageId The page's id. Can be null of the page is not yet created
1185     * @param parentId The id of parent page
1186     * @param pageTitle The title of page to create
1187     * @return the list of available services
1188     */
1189    @Callable
1190    public List<Map<String, Object>> getAvailableTemplatesForCreation (String pageId, String parentId, String pageTitle)
1191    {
1192        if (StringUtils.isNotEmpty(pageId))
1193        {
1194            // Get available template for a page
1195            return getAvailableTemplates(pageId);
1196        }
1197        else if (StringUtils.isNotEmpty(parentId))
1198        {
1199            // Get available template for a not yet existing page
1200            SitemapElement parent = _resolver.resolveById(parentId);
1201            
1202            // Create page temporarily
1203            Page page = _createPage(parent, pageTitle, null);
1204            
1205            List<Map<String, Object>> availableTemplates = getAvailableTemplates(page.getId());
1206            
1207            // Cancel page creation
1208            page.getSitemap().revertChanges();
1209            
1210            return availableTemplates;
1211        }
1212        
1213        return Collections.EMPTY_LIST;
1214    }
1215    
1216    /**
1217     * Get service info
1218     * @param pageId Optional, the page id of the service. To get some basic info about the page.
1219     * @param serviceId The id of the service
1220     * @return a Map containing some info about the service (label, url..)
1221     */
1222    @Callable
1223    public Map<String, Object> getServiceInfo(String pageId, String serviceId)
1224    {
1225        Map<String, Object> info = new HashMap<>();
1226        
1227        if (StringUtils.isNotEmpty(pageId))
1228        {
1229            SitemapElement sitemapElement = _resolver.resolveById(pageId);
1230            info.put("page-id", sitemapElement.getId());
1231            info.put("page-title", sitemapElement.getTitle());
1232        }
1233
1234        Service service = _serviceExtensionPoint.getExtension(serviceId);
1235        info.put("id", service.getId());
1236        info.put("label", service.getLabel());
1237        info.put("url", service.getURL());
1238        info.put("smallIcon", service.getSmallIcon());
1239        info.put("iconGlyph", service.getIconGlyph());
1240        info.put("iconDecorator", service.getIconDecorator());
1241        return info;
1242    }
1243    
1244    /**
1245     * Rename a page
1246     * @param pageId The id of page to rename
1247     * @param title The page's  title
1248     * @param longTitle The page's long title.
1249     * @param updatePath true to update page's path
1250     * @param createAlias true to create a alias
1251     * @return the result map
1252     */
1253    @Callable (right = "Web_Rights_Page_Rename", rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
1254    public Map<String, Object> renamePage (String pageId, String title, String longTitle, boolean updatePath, boolean createAlias)
1255    {
1256        Map<String, Object> result = new HashMap<>();
1257        
1258        Page page = _resolver.resolveById(pageId);
1259        
1260        if (!(page instanceof ModifiablePage))
1261        {
1262            throw new IllegalArgumentException("Can not rename a non-modifiable page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1263        }
1264        
1265        ModifiablePage mPage = (ModifiablePage) page;
1266        mPage.setTitle(title);
1267        mPage.setLongTitle(longTitle);
1268
1269        if (updatePath)
1270        {
1271            String oldPathInSitemap = page.getPathInSitemap();
1272            String oldPath = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html";
1273            String oldPathForChild = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/**.html";
1274
1275            String pageName = "";
1276            try
1277            {
1278                pageName = NameHelper.filterName(title);
1279            }
1280            catch (IllegalArgumentException e)
1281            {
1282                result.put("invalid-name", title);
1283                return result;
1284            }
1285            
1286            if (!page.getName().equals(pageName))
1287            {
1288                int index = 1;
1289                String initialPageName = pageName;
1290                SitemapElement parent = page.getParent();
1291                while (parent.hasChild(pageName))
1292                {
1293                    pageName = initialPageName + "-" + (index++);
1294                }
1295
1296                mPage.rename(pageName);
1297                
1298                if (createAlias)
1299                {
1300                    ModifiableTraversableAmetysObject rootNode = AliasHelper.getRootNode(page.getSite());
1301                    
1302                    DefaultAlias alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias");
1303                    alias.setUrl(oldPath);
1304                    alias.setTarget(page.getId());
1305                    alias.setType(TargetType.PAGE);
1306                    alias.setCreationDate(new Date());
1307
1308                    // Alias for child pages
1309                    alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias");
1310                    alias.setUrl(oldPathForChild);
1311                    alias.setTarget("/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/{1}.html");
1312                    alias.setType(TargetType.URL);
1313                    alias.setCreationDate(new Date());
1314
1315                    rootNode.saveChanges();
1316                }
1317                
1318                // Notify observers that the page has been renamed
1319                Map<String, Object> eventParams = new HashMap<>();
1320                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1321                eventParams.put("path.old.path", oldPathInSitemap);
1322                eventParams.put(ObservationConstants.ARGS_PAGE_PATH, page.getPathInSitemap());
1323                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_RENAMED, _currentUserProvider.getUser(), eventParams));
1324                
1325            }
1326            else
1327            {
1328                // Notify observers that the page's title has been modified
1329                Map<String, Object> eventParams = new HashMap<>();
1330                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1331                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1332            }
1333                
1334        }
1335        else
1336        {
1337            // Notify observers that the page's title has been modified
1338            Map<String, Object> eventParams = new HashMap<>();
1339            eventParams.put(ObservationConstants.ARGS_PAGE, page);
1340            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1341        }
1342        
1343        Sitemap sitemap = page.getSitemap();
1344        if (sitemap.needsSave())
1345        {
1346            sitemap.saveChanges();
1347        }
1348        
1349        result.put("id", page.getId());
1350        result.put("path", page.getPath());
1351        result.put("title", page.getTitle());
1352        
1353        return result;
1354    }
1355    
1356    /**
1357     * Delete a page and its sub-pages.
1358     * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true.
1359     * The newly created contents are deleted whatever the value if 'deleteBelongingContents'.
1360     * @param pageId the id of page to delete
1361     * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only
1362     * @return The id of deleted pages
1363     */
1364    @Callable
1365    public Map<String, Object> deletePage(String pageId, boolean deleteBelongingContents)
1366    {
1367        ModifiablePage page = _resolver.resolveById(pageId);
1368        
1369        // Check rights on parent
1370        if (!_canDelete(page))
1371        {
1372            throw new IllegalStateException("You do not have the rights to delete the page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1373        }
1374        
1375        return deletePage(page, deleteBelongingContents);
1376    }
1377    
1378    /**
1379     * Delete a page and its sub-pages.
1380     * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true.
1381     * The newly created contents are deleted whatever the value if 'deleteBelongingContents'.
1382     * @param page the page to delete
1383     * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only
1384     * @return The id of deleted pages
1385     */
1386    public Map<String, Object> deletePage(ModifiablePage page, boolean deleteBelongingContents)
1387    {
1388        Map<String, Object> result = new HashMap<>();
1389        
1390        List<String> contentToDelete = getDeleteablePageContentIds(page.getId(), !deleteBelongingContents);
1391        
1392        Sitemap sitemap = page.getSitemap();
1393        SitemapElement parent = page.getParent();
1394        String pagePathInSitemap = page.getPathInSitemap();
1395        
1396        List<String> childPagesIds = _getChildrenPageIds(page);
1397        
1398        Map<String, Object> eventParams = new HashMap<>();
1399        eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1400        eventParams.put(ObservationConstants.ARGS_PAGE_PARENT, parent);
1401        eventParams.put(ObservationConstants.ARGS_PAGE_PATH, pagePathInSitemap);
1402        eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
1403        eventParams.put(ObservationConstants.ARGS_PAGE_CONTENTS, getPageContents(page));
1404        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETING, _currentUserProvider.getUser(), eventParams));
1405        
1406        // FIXME API test if this is not modifiable
1407        page.getParent();
1408        page.remove();
1409        ((ModifiableAmetysObject) parent).saveChanges();
1410        
1411        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETED, _currentUserProvider.getUser(), eventParams));
1412        
1413        result.put("id", page.getId());
1414        result.put("childPages", childPagesIds);
1415        
1416        result.putAll(_contentDAO.deleteContents(contentToDelete, true));
1417        return result;
1418    }
1419    
1420    /**
1421     * Set pages as blank page
1422     * @param pageIds the id of pages to modify
1423     * @return the id of pages which succeeded or failed. 
1424     */
1425    @Callable
1426    public Map<String, Object> setBlank (List<String> pageIds)
1427    {
1428        Map<String, Object> result = new HashMap<>();
1429        
1430        List<String> successes = new ArrayList<>();
1431        List<Map<String, Object>> failures = new ArrayList<>();
1432        
1433        for (String pageId : pageIds)
1434        {
1435            try
1436            {
1437                Page page = _resolver.resolveById(pageId);
1438                if (!(page instanceof ModifiablePage))
1439                {
1440                    throw new IllegalArgumentException("Can not set page as blank a non-modifiable page " + pageId);
1441                }
1442                
1443                ModifiablePage mPage = (ModifiablePage) page;
1444                
1445                if (page.getType().equals(PageType.CONTAINER))
1446                {
1447                    // Remove zones
1448                    for (ModifiableZone zone : mPage.getZones())
1449                    {
1450                        zone.remove();
1451                    }
1452                }
1453                
1454                mPage.setType(PageType.NODE);
1455                mPage.getSitemap().saveChanges();
1456                
1457                successes.add(pageId);
1458                
1459                Map<String, Object> eventParams = new HashMap<>();
1460                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1461                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
1462            }
1463            catch (Exception e)
1464            {
1465                getLogger().error("Cannot set the page '" + pageId + "' as blank page", e);
1466                
1467                Map<String, Object> failure = new HashMap<>();
1468                failure.put("id", pageId);
1469                failure.put("error", e.toString());
1470                failures.add(failure);
1471            }
1472        }
1473
1474        result.put("success", successes);
1475        result.put("failure", failures);
1476        
1477        return result;
1478    }
1479    
1480    /**
1481     * Set a template to pages
1482     * @param pageIds the id of pages to update
1483     * @param templateName The template name
1484     * @return the id of pages which succeeded
1485     */
1486    @Callable
1487    public Map<String, Object> setTemplate (List<String> pageIds, String templateName)
1488    {
1489        return setTemplate(pageIds, templateName, true);
1490    }
1491    
1492    /**
1493     * Set a template to pages
1494     * @param pageIds the id of pages to update
1495     * @param templateName The template name
1496     * @param checkAvailableTemplate true if you want to check available template
1497     * @return the id of pages which succeeded
1498     */
1499    public Map<String, Object> setTemplate (List<String> pageIds, String templateName, boolean checkAvailableTemplate)
1500    {
1501        Map<String, Object> result = new HashMap<>();
1502        
1503        List<String> successes = new ArrayList<>();
1504        
1505        String defaultZoneName = null;
1506        
1507        for (String pageId : pageIds)
1508        {
1509            Page page = _resolver.resolveById(pageId);
1510            if (!(page instanceof ModifiablePage))
1511            {
1512                throw new IllegalArgumentException("Can not set template a non-modifiable page " + pageId);
1513            }
1514            
1515            ModifiablePage mPage = (ModifiablePage) page;
1516            
1517            if (defaultZoneName == null)
1518            {
1519                String skinId = page.getSite().getSkinId();
1520                SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(templateName);
1521                if (tpl == null)
1522                {
1523                    throw new IllegalStateException("Template '" + templateName + "' does not exist on skin '" + skinId + "'");
1524                }
1525                
1526                defaultZoneName = tpl.getDefaultZoneId();
1527            }
1528            
1529            if (checkAvailableTemplate && !_templatesHandler.getAvailablesTemplates(mPage).contains(templateName))
1530            {
1531                throw new IllegalStateException("Template '" + templateName + "' is not available for page '" + pageId + "'");
1532            }
1533            
1534            if (page.getType().equals(PageType.CONTAINER))
1535            {
1536                _removeOldZones (mPage, templateName);
1537            }
1538            
1539            mPage.setTemplate(templateName);
1540            mPage.setType(PageType.CONTAINER);
1541            mPage.getSitemap().saveChanges();
1542            
1543            successes.add(pageId);
1544            
1545            Map<String, Object> eventParams = new HashMap<>();
1546            eventParams.put(ObservationConstants.ARGS_PAGE, page);
1547            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
1548        }
1549        
1550        if (defaultZoneName != null)
1551        {
1552            result.put("zonename", defaultZoneName);
1553        }
1554        
1555        result.put("success", successes);
1556        
1557        return result;
1558    }
1559    
1560    /**
1561     * Get the script class name to execute to add or update this service 
1562     * @param serviceId the service id
1563     * @return the script class name. Can be empty
1564     */
1565    @Callable
1566    public String getServiceParametersAction (String serviceId)
1567    {
1568        try
1569        {
1570            Service service = _serviceExtensionPoint.getExtension(serviceId);
1571            return service.getParametersScript().getScriptClassname();
1572        }
1573        catch (IllegalArgumentException e)
1574        {
1575            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
1576        }
1577    }
1578    
1579    private void _removeOldZones (ModifiablePage page, String templateName)
1580    {
1581        String skinId = page.getSite().getSkinId();
1582        
1583        SkinTemplate oldTemplate = _skinsManager.getSkin(skinId).getTemplate(templateName);
1584        
1585        Map<String, SkinTemplateZone> templateZones = oldTemplate.getZones();
1586        
1587        for (ModifiableZone zone : page.getZones())
1588        {
1589            if (!templateZones.containsKey(zone.getName()))
1590            {
1591                zone.remove();
1592            }
1593        }
1594    }
1595    
1596    /**
1597     * Get the tags from the pages
1598     * @param pageIds The ids of the pages
1599     * @return the tags of the pages
1600     */
1601    @Callable
1602    public Set<String> getTags (List<String> pageIds)
1603    {
1604        Set<String> tags = new HashSet<>();
1605        
1606        for (String pageId : pageIds)
1607        {
1608            Page page = _resolver.resolveById(pageId);
1609            tags.addAll(page.getTags());
1610        }
1611        
1612        return tags;
1613    }
1614    
1615    /**
1616     * Tag a list of pages
1617     * @param pageIds The ids of pages to tag
1618     * @param tagNames The tags
1619     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1620     * @param contextualParameters Contextual parameters. Must contain the site name
1621     * @return the result
1622     */
1623    @Callable
1624    public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1625    {
1626        Map<String, Object> result = new HashMap<>();
1627        
1628        result.put("nomodifiable-pages", new ArrayList<Map<String, Object>>());
1629        result.put("invalid-tags", new ArrayList<String>());
1630        result.put("allright-pages", new ArrayList<Map<String, Object>>());
1631        
1632        for (String pageId : pageIds)
1633        {
1634            Page page = _resolver.resolveById(pageId);
1635            
1636            Map<String, Object> page2json = new HashMap<>();
1637            page2json.put("id", page.getId());
1638            page2json.put("title", page.getTitle());
1639            
1640            if (page instanceof ModifiablePage)
1641            {
1642                ModifiablePage mPage = (ModifiablePage) page;
1643                
1644                TagMode tagMode = TagMode.valueOf(mode);
1645                
1646                Set<String> oldTags = mPage.getTags();
1647                if (TagMode.REPLACE.equals(tagMode))
1648                {
1649                    // First delete old tags
1650                    for (String tagName : oldTags)
1651                    {
1652                        mPage.untag(tagName);
1653                    }
1654                }
1655                    
1656                
1657                // Then set new tags
1658                for (String tagName : tagNames)
1659                {
1660                    if (_isTagValid(page, tagName))
1661                    {
1662                        if (TagMode.REMOVE.equals(tagMode))
1663                        {
1664                            mPage.untag(tagName);
1665                        }
1666                        else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
1667                        {
1668                            mPage.tag(tagName);
1669                        }
1670                    }
1671                    else
1672                    {
1673                        @SuppressWarnings("unchecked")
1674                        List<String> invalidTags = (List<String>) result.get("invalid-tags");
1675                        invalidTags.add(tagName);
1676                    }
1677                }
1678                
1679                mPage.saveChanges();
1680                
1681                page2json.put("tags", page.getTags());
1682                @SuppressWarnings("unchecked")
1683                List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages");
1684                allRightPages.add(page2json);
1685                
1686                if (!oldTags.equals(page.getTags()))
1687                {
1688                    // Notify observers that the content has been tagged
1689                    Map<String, Object> eventParams = new HashMap<>();
1690                    eventParams.put(ObservationConstants.ARGS_PAGE, page);
1691                    eventParams.put(ObservationConstants.ARGS_PAGE_TAGS, page.getTags());
1692                    eventParams.put(ObservationConstants.ARGS_PAGE_OLD_TAGS, oldTags);
1693                    _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1694                }
1695            }
1696            else
1697            {
1698                @SuppressWarnings("unchecked")
1699                List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
1700                nomodifiablePages.add(page2json);
1701            }
1702        }
1703        
1704        return result;
1705    }
1706    
1707    /**
1708     * Tag a list of contents with the given tags
1709     * @param pageIds The ids of pages to tag
1710     * @param contentIds The ids of contents to tag
1711     * @param tagNames The tags
1712     * @param contextualParameters The contextual parameters
1713     * @return the result map
1714     */
1715    @Callable
1716    public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters)
1717    {
1718        return tag(pageIds, contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
1719    }
1720    
1721    /**
1722     * Tag a list of contents and/org pages
1723     * @param pageIds The ids of pages to tag
1724     * @param contentIds The ids of contents to tag
1725     * @param tagNames The tags
1726     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1727     * @param contextualParameters The contextual parameters
1728     * @return the result
1729     */
1730    @Callable
1731    public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1732    {
1733        // Tag pages
1734        Map<String, Object> result = tag(pageIds, tagNames, mode, contextualParameters);
1735        
1736        // Tag contents
1737        result.putAll(_contentDAO.tag(contentIds, tagNames, mode, contextualParameters));
1738        
1739        // Invalid tags are ignored
1740        result.remove("invalid-tags");
1741        return result;
1742    }
1743    
1744    /**
1745     * Test if a tag is valid for a specific page
1746     * @param page The page
1747     * @param tagName The tag name
1748     * @return True if the tag is valid
1749     */
1750    public boolean _isTagValid (Page page, String tagName)
1751    {
1752        Map<String, Object> params = new HashMap<>();
1753        params.put("siteName", page.getSiteName());
1754        CMSTag tag = _tagProvider.getTag(tagName, params);
1755        
1756        return tag.getTarget().getName().equals("PAGE");
1757    }
1758    
1759    /**
1760     * Returns the ids of the pages matching the tag
1761     * @param sitename The site id
1762     * @param lang The language code
1763     * @param tag The tag id
1764     * @return Array of pages ids
1765     */
1766    public List<String> findPagedIdsByTag(String sitename, String lang, String tag)
1767    {
1768        return _getMemoryPageTagCache().get(PageTagCacheKey.of(sitename, lang, tag), __ -> _computePagesIds(sitename, lang, tag))
1769                    .stream()
1770                    .filter(_resolver::hasAmetysObjectForId) // Cache remember for tag in default, we have to check if page exists in current workspace
1771                    .collect(Collectors.toList());
1772    }
1773    
1774    private List<String> _computePagesIds(String sitename, String lang, String tag)
1775    {
1776        
1777        Session defaultSession = null;
1778        try
1779        {
1780            List<String> pagesIds = new ArrayList<>();
1781            
1782            // Force default workspace to execute query
1783            defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE);
1784            
1785            String xpath = PageQueryHelper.getPageXPathQuery(sitename, lang, null, new TagExpression(Operator.EQ, tag), null);
1786            AmetysObjectIterable<Page> pages = _ametysObjectResolver.query(xpath, defaultSession);
1787            Iterator<Page> it = pages.iterator();
1788    
1789            while (it.hasNext())
1790            {
1791                pagesIds.add(it.next().getId());
1792            }
1793            
1794            return pagesIds;
1795        }
1796        catch (RepositoryException e)
1797        {
1798            throw new AmetysRepositoryException(e);
1799        }
1800        finally
1801        {
1802            if (defaultSession != null)
1803            {
1804                defaultSession.logout();
1805            }
1806        }
1807    }
1808
1809    private Cache<PageTagCacheKey, List<String>> _getMemoryPageTagCache()
1810    {
1811        return _cacheManager.get(MEMORY_PAGESTAGCACHE);
1812    }
1813    
1814    /**
1815     * Set the visible of pages
1816     * @param pageIds The id of pages
1817     * @param visible <code>true</code> to set pages as visible, <code>false</code> otherwise
1818     * @return The result map
1819     */
1820    @Callable
1821    public Map<String, Object> setVisibility (List<String> pageIds, boolean visible)
1822    {
1823        Map<String, Object> result = new HashMap<>();
1824        
1825        result.put("nomodifiable-pages", new ArrayList<Map<String, Object>>());
1826        result.put("allright-pages", new ArrayList<Map<String, Object>>());
1827        
1828        for (String id : pageIds)
1829        {
1830            Page page = _resolver.resolveById(id);
1831         
1832            Map<String, Object> page2json = new HashMap<>();
1833            page2json.put("id", page.getId());
1834            page2json.put("title", page.getTitle());
1835            
1836            if (page instanceof ModifiablePage)
1837            {
1838                ModifiablePage mPage = (ModifiablePage) page;
1839                mPage.setVisible(visible);
1840                mPage.saveChanges();
1841                
1842                Map<String, Object> eventParams = new HashMap<>();
1843                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1844                eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1845                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1846                
1847                @SuppressWarnings("unchecked")
1848                List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages");
1849                allRightPages.add(page2json);
1850            }
1851            else
1852            {
1853                @SuppressWarnings("unchecked")
1854                List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
1855                nomodifiablePages.add(page2json);
1856            }
1857        } 
1858        
1859        return result;
1860    }
1861    
1862    /**
1863     * Get the contents of a {@link Page} and its subpages which can be deleted.
1864     * A content is deleteable if user has right, the content is not locked and not referenced by other pages.
1865     * @param id The id of page
1866     * @return The list of deletable contents
1867     */
1868    @Callable
1869    public Map<String, Object> getDeleteablePageContents (String id)
1870    {
1871        Map<String, Object> results = new HashMap<>();
1872        
1873        results.put("deleteable-contents", new ArrayList<Map<String, Object>>());
1874        results.put("referenced-contents", new ArrayList<Map<String, Object>>());
1875        results.put("unauthorized-contents", new ArrayList<Map<String, Object>>());
1876        results.put("locked-contents", new ArrayList<Map<String, Object>>());
1877        
1878        Page page = _resolver.resolveById(id);
1879        List<Content> contents = getPageContents(page, true);
1880        
1881        for (Content content : contents)
1882        {
1883            Map<String, Object> contentParams = new HashMap<>();
1884            contentParams.put("id", content.getId());
1885            contentParams.put("title", content.getTitle(new Locale(page.getSitemapName())));
1886            contentParams.put("name", content.getName());
1887            
1888            if (_isReferenced(content))
1889            {
1890                // Content is referenced by at least another page
1891                @SuppressWarnings("unchecked")
1892                List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) results.get("referenced-contents");
1893                referencedContents.add(contentParams);
1894            }
1895            else if (!_contentDAO.canDelete(content))
1896            {
1897                @SuppressWarnings("unchecked")
1898                List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) results.get("unauthorized-contents");
1899                unauthorizedContents.add(contentParams);
1900            }
1901            else if (_isLocked(content))
1902            {
1903                // If the content is locked by other
1904                @SuppressWarnings("unchecked")
1905                List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
1906                lockedContents.add(contentParams);
1907            }
1908            else
1909            {
1910                Map<String, Object> content2json = new HashMap<>();
1911                content2json.put("id", content.getId());
1912                content2json.put("name", content.getName());
1913                content2json.put("title", content.getTitle(new Locale(page.getSitemapName())));
1914                content2json.put("isNew", _isNew(content));
1915                content2json.put("isShared", content instanceof SharedContent);
1916                content2json.put("hasShared", _sharedContentManager.hasSharedContents(content));
1917                
1918                @SuppressWarnings("unchecked")
1919                List<Map<String, Object>> allrightContents = (List<Map<String, Object>>) results.get("deleteable-contents");
1920                allrightContents.add(content2json);
1921            }
1922        }
1923        
1924        return results;
1925    }
1926    
1927    /**
1928     * Get the contents that belong to the {@link Page} and its sub-pages and that can be deleted.
1929     * A content is deleteable if user has right, the content is not locked and it's not referenced by other pages.
1930     * If 'onlyNewlyCreatedContents' is set to 'true', only newly created contents will be returned
1931     * @param pageId The id of page
1932     * @param onlyNewlyCreatedContents true to return only the newly created contents
1933     * @return The ids of deleteable contents
1934     */
1935    public List<String> getDeleteablePageContentIds (String pageId, boolean onlyNewlyCreatedContents)
1936    {
1937        List<String> contentsId = new ArrayList<>();
1938        
1939        Page page = _resolver.resolveById(pageId);
1940        
1941        List<Content> contents = getPageContents(page, true);
1942        
1943        for (Content content : contents)
1944        {
1945            if (_contentDAO.canDelete(content) && !_isLocked(content) && !_isReferenced(content))
1946            {
1947                if (!onlyNewlyCreatedContents || _isNew(content))
1948                {
1949                    contentsId.add(content.getId());
1950                }
1951            }
1952        }
1953        
1954        return contentsId;
1955    }
1956        
1957    /**
1958     * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem}
1959     * @param id The id of page or zone item
1960     * @return The list of unreferenced contents
1961     */
1962    @Callable
1963    public List<Map<String, Object>> getUnreferencedContents (String id)
1964    {
1965        List<Map<String, Object>> unreferencedContents = new ArrayList<>();
1966        
1967        Page page = _resolver.resolveById(id);
1968        List<Content> contents = getPageContents(page, true);
1969        
1970        for (Content content : contents)
1971        {
1972            if (!_isReferenced(content))
1973            {
1974                Map<String, Object> content2json = new HashMap<>();
1975                content2json.put("id", content.getId());
1976                content2json.put("name", content.getName());
1977                content2json.put("title", content.getTitle(new Locale(page.getSitemapName())));
1978                content2json.put("isNew", _isNew(content));
1979                content2json.put("isShared", content instanceof SharedContent);
1980                content2json.put("hasShared", _sharedContentManager.hasSharedContents(content));
1981                
1982                unreferencedContents.add(content2json);
1983            }
1984        }
1985        
1986        return unreferencedContents;
1987    }
1988    
1989    /**
1990     * Returns the page's attachments root node
1991     * @param id the page's id
1992     * @return The attachments' root node informations
1993     */
1994    @Callable
1995    public Map<String, Object> getAttachmentsRootNode (String id)
1996    {
1997        Map<String, Object> result = new HashMap<>();
1998        
1999        Page page = _resolver.resolveById(id);
2000        
2001        result.put("title", page.getTitle());
2002        result.put("contentId", page.getId());
2003        
2004        TraversableAmetysObject attachments = page.getRootAttachments();
2005        
2006        if (attachments != null)
2007        {
2008            result.put("id", attachments.getId());
2009            if (attachments instanceof ModifiableAmetysObject)
2010            {
2011                result.put("isModifiable", true);
2012            }
2013            if (attachments instanceof ModifiableResourceCollection)
2014            {
2015                result.put("canCreateChild", true);
2016            }
2017            
2018            boolean hasChildNodes = false;
2019            boolean hasResources = false;
2020
2021            for (AmetysObject child : attachments.getChildren())
2022            {
2023                if (child instanceof Resource)
2024                {
2025                    hasResources = true;
2026                }
2027                else if (child instanceof ExplorerNode)
2028                {
2029                    hasChildNodes = true;
2030                }
2031            }
2032
2033            if (hasChildNodes)
2034            {
2035                result.put("hasChildNodes", true);
2036            }
2037
2038            if (hasResources)
2039            {
2040                result.put("hasResources", true);
2041            }
2042            
2043            return result;
2044        }
2045        
2046        throw new IllegalArgumentException("Page with id '" + id + "' does not support attachments.");
2047    }
2048    /**
2049     * Returns the page's parents ids
2050     * @param id the page's id
2051     * @return The attachments' root node informations
2052     */
2053    @Callable
2054    public Map<String, Object> getPageParents (String id)
2055    {
2056        Map<String, Object> result = new HashMap<>();
2057        List<Map<String, Object>> pages = new ArrayList<>();
2058        Page page = _resolver.resolveById(id);
2059        pages.add(_page2Json(page));
2060        while (page.getParent() != null && page.getParent() instanceof Page)
2061        {
2062            page = page.getParent();
2063            pages.add(_page2Json(page));
2064        }
2065        result.put("parents", pages);
2066        return result;
2067    }
2068    
2069    /**
2070     * Get the contents of a page and its child pages
2071     * @param sitemapElement The page
2072     * @return The list of contents
2073     */
2074    public List<Content> getPageContents (SitemapElement sitemapElement)
2075    {
2076        return getPageContents(sitemapElement, false);
2077    }
2078    
2079    /**
2080     * Get the contents of a page and its child pages
2081     * @param sitemapElement The page
2082     * @param ignoreContentsOfNonRemovablePage true to ignore contents of non-removable pages (virtual pages)
2083     * @return The list of contents
2084     */
2085    public List<Content> getPageContents (SitemapElement sitemapElement, boolean ignoreContentsOfNonRemovablePage)
2086    {
2087        List<Content> contents = new ArrayList<>();
2088        
2089        if ((!ignoreContentsOfNonRemovablePage || sitemapElement instanceof RemovableAmetysObject) 
2090                && sitemapElement.getTemplate() != null)
2091        {
2092            for (Zone zone : sitemapElement.getZones())
2093            {
2094                try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems())
2095                {
2096                    for (ZoneItem zoneItem : zoneItems)
2097                    {
2098                        if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT)
2099                        {
2100                            contents.add(zoneItem.getContent());
2101                        }
2102                    }
2103                }
2104            }
2105        }
2106        
2107        AmetysObjectIterable< ? extends Page> childrenPages = sitemapElement.getChildrenPages();
2108        for (Page childPage : childrenPages)
2109        {
2110            contents.addAll(getPageContents(childPage, ignoreContentsOfNonRemovablePage));
2111        }
2112        
2113        return contents;
2114    }
2115    
2116    /**
2117     * Get the user rights on sitemap element (page or sitemap)
2118     * @param pagesCt The sitemap element
2119     * @return The user's rights
2120     */
2121    protected Set<String> getUserRights (SitemapElement pagesCt)
2122    {
2123        UserIdentity user = _currentUserProvider.getUser();
2124        
2125        Set<String> userRights = _rightManager.getUserRights(user, pagesCt);
2126        
2127        // Do some specific stuff here, because the right 'Web_Rights_Page_Delete' is a right to delete child pages and not the page itself.
2128        // So the right should be checked on parent context.
2129        if (pagesCt instanceof Page)
2130        {
2131            SitemapElement parent = pagesCt.getParent();
2132            boolean canDelete = _rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW;
2133            if (!canDelete)
2134            {
2135                // No right on parent page, so remove the right if exists.
2136                userRights.remove("Web_Rights_Page_Delete");
2137            }
2138        }
2139        else
2140        {
2141            // There is no right of deletion on the sitemap
2142            userRights.remove("Web_Rights_Page_Delete");
2143        }
2144        
2145        return userRights;
2146    }
2147    
2148    private boolean _isReferenced (Content content)
2149    {
2150        return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1;
2151    }
2152    
2153    private boolean _isLocked (Content content)
2154    {
2155        if (content instanceof LockableAmetysObject)
2156        {
2157            LockableAmetysObject lockableContent = (LockableAmetysObject) content;
2158            if (lockableContent.isLocked())
2159            {
2160                boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW;
2161                if (!LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) && !canUnlockAll)
2162                {
2163                    return true;
2164                }
2165            }
2166        }
2167        
2168        return false;
2169    }
2170    
2171    private boolean _isNew (Content content)
2172    {
2173        boolean isNew = false;
2174        if (content instanceof WorkflowAwareContent)
2175        {
2176            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
2177            long workflowId = waContent.getWorkflowId();
2178            
2179            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
2180            isNew = workflow.getHistorySteps(workflowId).isEmpty();
2181        }
2182        return isNew;
2183    }
2184
2185    private Map<String, Object> _page2Json (Page page)
2186    {
2187        Map<String, Object> page2json = new HashMap<>();
2188        page2json.put("id", page.getId());
2189        page2json.put("title", page.getTitle());
2190        page2json.put("siteName", page.getSiteName());
2191        page2json.put("path", page.getPathInSitemap());
2192        return page2json;
2193    }
2194    
2195    private Map<String, Object> _content2Json (Content content, Locale locale)
2196    {
2197        Map<String, Object> content2json = new HashMap<>();
2198        content2json.put("id", content.getId());
2199        content2json.put("title", content.getTitle(locale));
2200        content2json.put("name", content.getName());
2201        
2202        List<Map<String, Object>> pages = new ArrayList<>();
2203        if (content instanceof WebContent)
2204        {
2205            content2json.put("siteName", ((WebContent) content).getSiteName());
2206            Collection<Page> refPages = ((WebContent) content).getReferencingPages();
2207            for (Page refPage : refPages)
2208            {
2209                pages.add(_page2Json(refPage));
2210            }
2211            content2json.put("pages", pages);
2212        }
2213        
2214        return content2json;
2215    }
2216    
2217    private Map<String, Object> _publication2Json (Page page)
2218    {
2219        Map<String, Object> pub2json = new HashMap<>();
2220        @SuppressWarnings("unchecked")
2221        ElementType<ZonedDateTime> dateType = (ElementType<ZonedDateTime>) _pageDataTypeExtensionPoint.getExtension(ModelItemTypeConstants.DATETIME_TYPE_ID);
2222        
2223        ZonedDateTime startDate = page.getValue(DefaultPage.METADATA_PUBLICATION_START_DATE);
2224        if (startDate != null)
2225        {
2226            pub2json.put("startDate", dateType.valueToJSONForClient(startDate, DataContext.newInstance()));
2227        }
2228
2229        ZonedDateTime endDate = page.getValue(DefaultPage.METADATA_PUBLICATION_END_DATE);
2230        if (endDate != null)
2231        {
2232            pub2json.put("endDate", dateType.valueToJSONForClient(endDate, DataContext.newInstance()));
2233        }
2234        
2235        return pub2json;
2236        
2237    }
2238    
2239    private AmetysObjectIterable<Content> _getIncomingContentReferences (String pageId)
2240    {
2241        String xpathQuery = "//element(*, ametys:content)[ametys-internal:consistency/@ametys-internal:link = 'page:" + pageId + "']";
2242        return _resolver.query(xpathQuery);
2243    }
2244    
2245    private AmetysObjectIterable<Page> _getIncomingPageReferences (String pageId)
2246    {
2247        String xpathQuery = "//element(*, ametys:page)[@ametys-internal:type = 'LINK' and @ametys-internal:url= '" + pageId + "']";
2248        return _resolver.query(xpathQuery);
2249    }
2250    
2251    private void _updateContentsAfterCopy(Page initialPage, Page createdPage) throws AmetysRepositoryException
2252    {
2253        for (Zone zone : createdPage.getZones())
2254        {
2255            Zone initialZone = initialPage.getZone(zone.getName());
2256            try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems(); AmetysObjectIterable< ? extends ZoneItem> initialZoneItems = initialZone.getZoneItems())
2257            {
2258                AmetysObjectIterator<? extends ZoneItem> iterator = zoneItems.iterator();
2259                AmetysObjectIterator< ? extends ZoneItem> initialIterator = initialZoneItems.iterator();
2260                if (iterator.getSize() != initialIterator.getSize())
2261                {
2262                    throw new IllegalStateException("An error occured during the copy of " + initialPage.getName() + " (" + initialPage.getId() + "). The resulting page have a different number of zoneItems.");
2263                }
2264                while (iterator.hasNext())
2265                {
2266                    ZoneItem zoneItem = iterator.next();
2267                    ZoneItem initialZoneItem = initialIterator.next();
2268                    if (zoneItem.getType().equals(ZoneType.CONTENT))
2269                    {
2270                        Content content = zoneItem.getContent();
2271                        WebContent initialContent = initialZoneItem.getContent();
2272                        
2273                        // Updaters
2274                        Set<String> ids = _copyUpdaterEP.getExtensionsIds();
2275                        for (String id : ids)
2276                        {
2277                            _copyUpdaterEP.getExtension(id).updateContent(initialPage.getSite(), createdPage.getSite(), initialContent, content);
2278                        }
2279                        
2280                        // Convert content language if necessary
2281                        if (content instanceof ModifiableContent && content.getLanguage() != null && content.getLanguage() != createdPage.getSitemapName())
2282                        {
2283                            ((ModifiableContent) content).setLanguage(createdPage.getSitemapName());
2284                            ((ModifiableContent) content).saveChanges();
2285                        }
2286                        
2287                        // Create the first version
2288                        if (content instanceof VersionableAmetysObject)
2289                        {
2290                            ((VersionableAmetysObject) content).checkpoint();
2291                        }
2292                    }
2293                }
2294            }
2295        }
2296        
2297        // Browse child pages
2298        for (Page childPage : createdPage.getChildrenPages())
2299        {
2300            Page initialChildPage = initialPage.getChild(childPage.getName());
2301            _updateContentsAfterCopy (initialChildPage, childPage);
2302        }
2303    }
2304    
2305    private List<String> _getChildrenPageIds (Page page)
2306    {
2307        List<String> childIds = new ArrayList<>();
2308        
2309        for (Page childPage : page.getChildrenPages())
2310        {
2311            childIds.add(childPage.getId());
2312            childIds.addAll(_getChildrenPageIds(childPage));
2313        }
2314        
2315        return childIds;
2316    }
2317
2318    /**
2319     * Check each parent and return true if one of them is invisible
2320     * @param page page to check
2321     * @return true if at least one parent is invisible
2322     */
2323    private boolean _isParentInvisible (Page page)
2324    {
2325        AmetysObject parent = page.getParent();
2326        while (parent != null && parent instanceof Page)
2327        {
2328            boolean invisible = !((Page) parent).isVisible();
2329            if (invisible)
2330            {
2331                return true;
2332            }
2333            parent = parent.getParent();
2334        }
2335        return false;
2336    }
2337
2338    /* start of a group of methods for _isPreviewable */
2339    /**
2340     * Determine if this page is previewable
2341     * @param page The page to look at
2342     * @return true if the page can be previewed
2343     */
2344    private boolean _isPreviewable(Page page)
2345    {
2346        // Check for infinitive loop redirection
2347        ArrayList<String> pagesSequence = new ArrayList<>();
2348        pagesSequence.add(page.getId());
2349        if (_isInfiniteRedirection (page, pagesSequence))
2350        {
2351            getLogger().error("An infinite loop redirection was detected for page '" + page.getPathInSitemap() + "'");
2352            return false;
2353        }
2354
2355        if (page.getType() == PageType.LINK && LinkType.PAGE.equals(page.getURLType()))
2356        {
2357            return _isPageExist(page.getURL()) && _isPreviewable((Page) _resolver.resolveById(page.getURL()));
2358        }
2359
2360        if (page.getType() != PageType.NODE)
2361        {
2362            return true;
2363        }
2364        else
2365        {
2366            try (AmetysObjectIterable< ? extends Page> childrenPages = page.getChildrenPages())
2367            {
2368                for (Page subPage : childrenPages)
2369                {
2370                    if (_isPreviewable(subPage))
2371                    {
2372                        return true;
2373                    }
2374                }
2375            }
2376        }
2377        return false;
2378    }
2379
2380    private boolean _isPageExist (String id)
2381    {
2382        try
2383        {
2384            _resolver.resolveById(id);
2385            return true;
2386        }
2387        catch (UnknownAmetysObjectException e)
2388        {
2389            return false;
2390        }
2391    }
2392
2393    private boolean _isInfiniteRedirection (Page page, List<String> pagesSequence)
2394    {
2395        Page redirectPage = _getPageRedirection (page);
2396        if (redirectPage == null)
2397        {
2398            return false;
2399        }
2400
2401        if (pagesSequence.contains(redirectPage.getId()))
2402        {
2403            return true;
2404        }
2405
2406        pagesSequence.add(redirectPage.getId());
2407        return _isInfiniteRedirection (redirectPage, pagesSequence);
2408    }
2409
2410    private Page _getPageRedirection (Page page)
2411    {
2412        if (PageType.LINK.equals(page.getType()) && LinkType.PAGE.equals(page.getURLType()))
2413        {
2414            try
2415            {
2416                String pageId = page.getURL();
2417                return _resolver.resolveById(pageId);
2418            }
2419            catch (AmetysRepositoryException e)
2420            {
2421                return null;
2422            }
2423        }
2424        else if (PageType.NODE.equals(page.getType()))
2425        {
2426            AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages();
2427            Iterator<? extends Page> it = childPages.iterator();
2428            if (it.hasNext())
2429            {
2430                return it.next();
2431            }
2432        }
2433
2434        return null;
2435    }
2436    /* end of a group of methods for _isPreviewable */
2437    
2438    private static final class PageTagCacheKey extends AbstractCacheKey
2439    {
2440        private PageTagCacheKey(String sitename, String lang, String tag)
2441        {
2442            super(sitename, lang, tag);
2443        }
2444        
2445        private PageTagCacheKey(String sitename, String lang)
2446        {
2447            super(sitename, lang);
2448        }
2449
2450        static PageTagCacheKey of(String sitename, String lang)
2451        {
2452            return new PageTagCacheKey(sitename, lang, null);
2453        }
2454
2455        static PageTagCacheKey of(String sitename, String lang, String tag)
2456        {
2457            return new PageTagCacheKey(sitename, lang, tag);
2458        }
2459    }
2460}