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