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