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