001/*
002 *  Copyright 2010 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.jcr;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.function.Consumer;
026
027import javax.jcr.ItemExistsException;
028import javax.jcr.ItemNotFoundException;
029import javax.jcr.Node;
030import javax.jcr.Property;
031import javax.jcr.PropertyType;
032import javax.jcr.RepositoryException;
033import javax.jcr.Value;
034
035import org.apache.commons.lang3.StringUtils;
036
037import org.ametys.plugins.explorer.resources.ResourceCollection;
038import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
039import org.ametys.plugins.repository.AmetysObject;
040import org.ametys.plugins.repository.AmetysObjectIterable;
041import org.ametys.plugins.repository.AmetysRepositoryException;
042import org.ametys.plugins.repository.CopiableAmetysObject;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.repository.RepositoryConstants;
045import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
046import org.ametys.plugins.repository.UnknownAmetysObjectException;
047import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
048import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
049import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
050import org.ametys.plugins.repository.jcr.NodeHelper;
051import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
052import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
053import org.ametys.plugins.repository.trash.TrashElement;
054import org.ametys.plugins.repository.trash.TrashableAmetysObject;
055import org.ametys.plugins.repository.trash.UnknownParentException;
056import org.ametys.runtime.model.exception.BadItemTypeException;
057import org.ametys.runtime.model.exception.NotUniqueTypeException;
058import org.ametys.runtime.model.exception.UnknownTypeException;
059import org.ametys.runtime.model.type.DataContext;
060import org.ametys.runtime.model.type.ModelItemTypeConstants;
061import org.ametys.web.parameters.view.ViewParametersManager;
062import org.ametys.web.repository.page.LockablePage;
063import org.ametys.web.repository.page.ModifiableZone;
064import org.ametys.web.repository.page.MoveablePage;
065import org.ametys.web.repository.page.Page;
066import org.ametys.web.repository.page.PageDAO;
067import org.ametys.web.repository.page.SitemapElement;
068import org.ametys.web.repository.page.Zone;
069import org.ametys.web.repository.page.ZoneItem;
070import org.ametys.web.repository.site.Site;
071import org.ametys.web.repository.sitemap.Sitemap;
072
073/**
074 * {@link Page} implementation stored into a JCR node using
075 * <code>ametys:defaultPage</code> node type.
076 * 
077 * @param <F> the actual type of factory.
078 */
079public class DefaultPage<F extends DefaultPageFactory> extends AbstractSitemapElement<F> implements LockablePage, CopiableAmetysObject, MoveablePage, TrashableAmetysObject
080{
081    /** Constant for template metadata. */
082    public static final String METADATA_TEMPLATE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":template";
083
084    /** Constant for title metadata. */
085    public static final String METADATA_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title";
086
087    /** Constant for the initial title metadata. */
088    public static final String METADATA_INITIAL_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title-initial";
089
090    /** Constant for long title metadata. */
091    public static final String METADATA_LONG_TITLE = "long-title";
092
093    /** Constant for title metadata. */
094    public static final String METADATA_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":type";
095
096    /** Constant for title metadata. */
097    public static final String METADATA_URL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url";
098
099    /** Constant for title metadata. */
100    public static final String METADATA_URLTYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url-type";
101
102    /** Constant for referers metadata. */
103    public static final String METADATA_REFERERS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":referers";
104    
105    /** Constant for the visible attribute. */
106    public static final String METADATA_VISIBLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":visible";
107    
108    /** Constant for the locked attribute. */
109    public static final String METADATA_LOCKED = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":locked";
110    
111    /** Constant for the visible attribute. */
112    public static final String METADATA_INDEXABLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":indexable";
113
114    /** Constant for robots metadata. */
115    public static final String METADATA_ROBOTS_DISALLOW = "robots-disallow";
116    
117    /** Constant for the attachment node name. */
118    public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments";
119    
120    /** Constants for sitemap Metadata* */
121    public static final String METADATA_SITEMAP = "sitemap";
122
123    /** Constant for publication start date metadata. */
124    public static final String METADATA_PUBLICATION_START_DATE = "publicationStartDate";
125    
126    /** Constant for publication end date metadata. */
127    public static final String METADATA_PUBLICATION_END_DATE = "publicationEndDate";
128    
129    /**
130     * Creates a {@link DefaultPage}.
131     * 
132     * @param node the node backing this {@link AmetysObject}.
133     * @param parentPath the parent path in the Ametys hierarchy.
134     * @param factory the {@link DefaultPageFactory} which creates the
135     *            AmetysObject.
136     */
137    public DefaultPage(Node node, String parentPath, F factory)
138    {
139        super(node, parentPath, factory);
140    }
141    
142    @Override
143    public String getTemplate() throws AmetysRepositoryException
144    {
145        if (getType() != PageType.CONTAINER)
146        {
147            return null;
148        }
149        
150        try
151        {
152            Node node = getNode();
153            
154            if (node.hasProperty(METADATA_TEMPLATE))
155            {
156                return node.getProperty(METADATA_TEMPLATE).getString();
157            }
158            else
159            {
160                return null;
161            }
162        }
163        catch (RepositoryException e)
164        {
165            throw new AmetysRepositoryException("Unable to get template property", e);
166        }
167    }
168
169    @Override
170    public void setTemplate(String template) throws AmetysRepositoryException
171    {
172        try
173        {
174            getNode().setProperty(METADATA_TEMPLATE, template);
175        }
176        catch (RepositoryException e)
177        {
178            throw new AmetysRepositoryException("Unable to set template property", e);
179        }
180    }
181
182    @Override
183    public Set<String> getTags() throws AmetysRepositoryException
184    {
185        return TaggableAmetysObjectHelper.getTags(this);
186    }
187    
188    @Override
189    public void tag(String tag) throws AmetysRepositoryException
190    {
191        TaggableAmetysObjectHelper.tag(this, tag);
192    }
193
194    @Override
195    public void untag(String tag) throws AmetysRepositoryException
196    {
197        TaggableAmetysObjectHelper.untag(this, tag);
198    }
199
200    @Override
201    public String getTitle() throws AmetysRepositoryException
202    {
203        try
204        {
205            return getNode().getProperty(METADATA_TITLE).getString();
206        }
207        catch (RepositoryException e)
208        {
209            throw new AmetysRepositoryException("Unable to get title property", e);
210        }
211    }
212    
213    @Override
214    public void setTitle(String title) throws AmetysRepositoryException
215    {
216        try
217        {
218            getNode().setProperty(METADATA_TITLE, title);
219        }
220        catch (RepositoryException e)
221        {
222            throw new AmetysRepositoryException("Unable to set title property", e);
223        }
224        setInitialTitle(title);
225    }
226
227    /**
228     * Get the title that was set by the user (and not set by a copy adding a number after)
229     * @return The initial title
230     * @throws AmetysRepositoryException if an error occurs
231     */
232    public String getInitialTitle()  throws AmetysRepositoryException
233    {
234        try
235        {
236            if (getNode().hasProperty(METADATA_INITIAL_TITLE))
237            {
238                return getNode().getProperty(METADATA_INITIAL_TITLE).getString();
239            }
240            else
241            {
242                return getTitle();
243            }
244        }
245        catch (RepositoryException e)
246        {
247            throw new AmetysRepositoryException("Unable to get initial title property", e);
248        }
249    }
250    
251    /**
252     * Set the title that was set by the user (and not set by a copy adding a number after)
253     * @param initialTitle The initial title to set
254     * @throws AmetysRepositoryException if an error occurs
255     */
256    public void setInitialTitle(String initialTitle) throws AmetysRepositoryException
257    {
258        try
259        {
260            getNode().setProperty(METADATA_INITIAL_TITLE, initialTitle);
261        }
262        catch (RepositoryException e)
263        {
264            throw new AmetysRepositoryException("Unable to set initial title property", e);
265        }
266    }
267
268    @Override
269    public String getLongTitle() throws AmetysRepositoryException
270    {
271        String longTitle = getValue(METADATA_LONG_TITLE);
272        if (StringUtils.isNotBlank(longTitle))
273        {
274            return longTitle;
275        }
276        else
277        {
278            return this.getTitle();
279        }
280    }
281
282    @Override
283    public void setLongTitle(String title) throws AmetysRepositoryException
284    {
285        String newTitle = StringUtils.trimToNull(title);
286        setValue(METADATA_LONG_TITLE, newTitle, ModelItemTypeConstants.STRING_TYPE_ID);
287    }
288
289    @Override
290    public Sitemap getSitemap() throws AmetysRepositoryException
291    {
292        try
293        {
294            Node node = getNode();
295
296            do
297            {
298                node = node.getParent();
299            }
300            while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE));
301
302            return _getFactory().resolveAmetysObject(node);
303        }
304        catch (RepositoryException e)
305        {
306            throw new AmetysRepositoryException("Unable to get sitemap", e);
307        }
308    }
309    
310    @Override
311    public int getDepth() throws AmetysRepositoryException
312    {
313        try
314        {
315            int depth = 1;
316            Node node = getNode().getParent();
317            while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE))
318            {
319                depth++;
320                node = node.getParent();
321            }
322
323            return depth;
324        }
325        catch (RepositoryException e)
326        {
327            throw new AmetysRepositoryException("Unable to get depth", e);
328        }
329    }
330
331    @Override
332    public Site getSite() throws AmetysRepositoryException
333    {
334        return getSitemap().getSite();
335    }
336    
337    @Override
338    public String getSiteName()
339    {
340        return getValue(METADATA_SITE);
341    }
342    
343    @Override
344    public void setSiteName(String siteName)
345    {
346        setValue(METADATA_SITE, siteName);
347    }
348    
349    @Override
350    public String getSitemapName() throws AmetysRepositoryException
351    {
352        return getValue(METADATA_SITEMAP);
353    }
354    
355    @Override
356    public void setSitemapName(String sitemapName) throws AmetysRepositoryException
357    {
358        setValue(METADATA_SITEMAP, sitemapName);
359    }
360
361    @Override
362    public String getPathInSitemap() throws AmetysRepositoryException
363    {
364        String nodePath = getPath();
365        String sitemapPath = getSitemap().getPath();
366        return nodePath.substring(sitemapPath.length() + 1);
367    }
368
369    @Override
370    public PageType getType() throws AmetysRepositoryException
371    {
372        try
373        {
374            return PageType.valueOf(getNode().getProperty(METADATA_TYPE).getString());
375        }
376        catch (RepositoryException e)
377        {
378            throw new AmetysRepositoryException("Unable to get type property", e);
379        }
380    }
381
382    @Override
383    public void setType(PageType type) throws AmetysRepositoryException
384    {
385        try
386        {
387            getNode().setProperty(METADATA_TYPE, type.name());
388        }
389        catch (RepositoryException e)
390        {
391            throw new AmetysRepositoryException("Unable to set type property", e);
392        }
393    }
394
395    @Override
396    public String getURL() throws AmetysRepositoryException
397    {
398        try
399        {
400            return getNode().getProperty(METADATA_URL).getString();
401        }
402        catch (RepositoryException e)
403        {
404            throw new AmetysRepositoryException("Unable to get url property", e);
405        }
406    }
407    
408    @Override
409    public LinkType getURLType() throws AmetysRepositoryException
410    {
411        try
412        {
413            return LinkType.valueOf(getNode().getProperty(METADATA_URLTYPE).getString());
414        }
415        catch (RepositoryException e)
416        {
417            throw new AmetysRepositoryException("Unable to get url type property", e);
418        }
419    }
420
421    @Override
422    public void setURL(LinkType type, String url) throws AmetysRepositoryException
423    {
424        try
425        {
426            getNode().setProperty(METADATA_URLTYPE, type.toString());
427            getNode().setProperty(METADATA_URL, url);
428        }
429        catch (RepositoryException e)
430        {
431            throw new AmetysRepositoryException("Unable to set url property", e);
432        }
433    }
434
435    @Override
436    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
437    {
438        ResourceCollection attachments;
439        if (hasChild(ATTACHMENTS_NODE_NAME))
440        {
441            attachments = getChild(ATTACHMENTS_NODE_NAME);
442        }
443        else
444        {
445            attachments = createChild(ATTACHMENTS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE);
446            
447            try
448            {
449                getNode().getSession().save();
450            }
451            catch (RepositoryException e)
452            {
453                throw new AmetysRepositoryException("Unable to save session", e);
454            }
455        }
456        
457        return attachments;
458    }
459    
460    @Override
461    public Set<String> getReferers() throws AmetysRepositoryException
462    {
463        try
464        {
465            Node node = getNode();
466
467            if (!node.hasProperty(METADATA_REFERERS))
468            {
469                return Collections.emptySet();
470            }
471
472            Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
473            Set<String> ids = new HashSet<>(values.length);
474
475            for (Value value : values)
476            {
477                ids.add(value.getString());
478            }
479
480            return ids;
481        }
482        catch (RepositoryException e)
483        {
484            throw new AmetysRepositoryException("Unable to get referers property", e);
485        }
486    }
487
488    @Override
489    public void addReferer(String ametysObjectId) throws AmetysRepositoryException
490    {
491        try
492        {
493            Set<String> ids = null;
494            Node node = getNode();
495
496            if (!node.hasProperty(METADATA_REFERERS))
497            {
498                ids = Collections.singleton(ametysObjectId);
499            }
500            else
501            {
502                Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
503                ids = new HashSet<>(values.length + 1);
504
505                for (Value value : values)
506                {
507                    ids.add(value.getString());
508                }
509
510                ids.add(ametysObjectId);
511            }
512
513            node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()]));
514        }
515        catch (RepositoryException e)
516        {
517            throw new AmetysRepositoryException("Unable to add an id into referers property", e);
518        }
519    }
520
521    @Override
522    public void removeReferer(String ametysObjectId) throws AmetysRepositoryException
523    {
524        try
525        {
526            Node node = getNode();
527
528            if (node.hasProperty(METADATA_REFERERS))
529            {
530                Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
531                Set<String> ids = new HashSet<>(values.length + 1);
532
533                for (Value value : values)
534                {
535                    ids.add(value.getString());
536                }
537
538                ids.remove(ametysObjectId);
539                node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()]));
540            }
541        }
542        catch (RepositoryException e)
543        {
544            throw new AmetysRepositoryException("Unable to remove an id from referers property", e);
545        }
546    }
547    
548    @Override
549    public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
550    {
551        try
552        {
553            String pageName = PageDAO.findFreePageName(parent, name != null ? name : getName());
554            
555            DefaultPage cPage = parent.createChild(pageName, "ametys:defaultPage");
556            cPage.setType(getType());
557            cPage.setTitle(getTitle());
558            cPage.setInitialTitle(getInitialTitle());
559            
560            if (PageType.CONTAINER.equals(getType()))
561            {
562                cPage.setTemplate(getTemplate());
563            }
564            else if (PageType.LINK.equals(getType()))
565            {
566                cPage.setURL(getURLType(), getURL());
567            }
568            
569            // Copy metadata
570            copyTo(cPage);
571            
572            // Copy view parameters
573            getTemplateParametersHolder().copyTo(cPage.getTemplateParametersHolder());
574            
575            // Copy zones
576            for (ModifiableZone zone : getZones())
577            {
578                if (zone instanceof CopiableAmetysObject copiableZone)
579                {
580                    copiableZone.copyTo(cPage, (String) null);
581                }
582            }
583            
584            // Copy tags
585            Set<String> tags = getTags();
586            for (String tag : tags)
587            {
588                cPage.tag(tag);
589            }
590            
591            // Update sitemap name
592            if (parent instanceof SitemapElement)
593            {
594                cPage.setSitemapName(((SitemapElement) parent).getSitemapName());
595            }
596            
597            parent.saveChanges();
598            
599            // Copy attachments
600            ResourceCollection rootAttachments = getRootAttachments();
601            if (rootAttachments instanceof SimpleAmetysObject)
602            {
603                Node resourcesNode = ((SimpleAmetysObject) rootAttachments).getNode();
604                getNode().getSession().getWorkspace().copy(resourcesNode.getPath(), cPage.getNode().getPath() + "/" + resourcesNode.getName());
605            }
606            
607            // Copy sub-pages
608            AmetysObjectIterable< ? extends Page> childrenPages = getChildrenPages();
609            for (Page page : childrenPages)
610            {
611                // Avoid infinite loop : do not copy the page itself
612                if (page instanceof CopiableAmetysObject && restrictTo.contains(page.getId()))
613                {
614                    ((CopiableAmetysObject) page).copyTo(cPage, null, restrictTo);
615                }
616            }
617            
618            return cPage;
619        }
620        catch (RepositoryException e)
621        {
622            throw new AmetysRepositoryException(e);
623        }
624    }
625    
626    @Override
627    public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
628    {
629        return copyTo (parent, name, new ArrayList<>());
630    }
631    
632    @Override
633    public void copyTo(ModifiableDataHolder dataHolder) throws UnknownTypeException, NotUniqueTypeException, BadItemTypeException
634    {
635        for (String name : getDataNames())
636        {
637            if (!_getExcludedDataFromCopy().contains(name))
638            {
639                DataHolderHelper.copyTo(this, dataHolder, name, DataContext.newInstance());
640            }
641        }
642    }
643    
644    /**
645     * Get the set of data name to exclude from the copy
646     * @return the set of data name to exclude from the copy
647     */
648    protected Set<String> _getExcludedDataFromCopy()
649    {
650        return Set.of(
651            ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME,
652            METADATA_PUBLICATION_START_DATE,
653            METADATA_PUBLICATION_END_DATE
654        );
655    }
656    
657    @Override
658    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
659    {
660        Node node = getNode();
661
662        try
663        {
664            if (getParent().equals(newParent))
665            {
666                // Just order current node to the end
667                node.getParent().orderBefore(node.getName(), null);
668            }
669            else
670            {
671                if (!canMoveTo(newParent))
672                {
673                    throw new AmetysRepositoryException("DefaultPage " + toString() + " can only be moved to a CompositePages and ");
674                }
675                SitemapElement newParentPage = (SitemapElement) newParent;
676                
677                String sitemapNodePath = getSitemap().getNode().getPath();
678                String newPath = sitemapNodePath + (newParentPage instanceof Sitemap ? "" : '/' + newParentPage.getPathInSitemap());
679
680                String name = node.getName();
681                
682                if (renameIfExist)
683                {
684                    // Find unused name on new parent node
685                    int index = 1;
686                    while (node.getSession().getRootNode().hasNode(newPath.substring(1) + "/" + name))
687                    {
688                        name = name + "-" + index++;
689                    }
690                }
691
692                try
693                {
694                    // Move node
695                    node.getSession().move(node.getPath(), newPath + "/" + name);
696                }
697                catch (ItemExistsException e)
698                {
699                    throw new AmetysRepositoryException(String.format("A page already exists for in parent path '%s'", newPath), e);
700                }
701                
702                // recompute name in case it has changed
703                _invalidateName();
704
705                // Invalidate parent path as the parent path has changed
706                _invalidateParentPath();
707            }
708        }
709        catch (RepositoryException e)
710        {
711            throw new AmetysRepositoryException(String.format("Unable to move page '%s' to node '%s'", this, newParent.getId()), e);
712        }
713    }
714    
715    @Override
716    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
717    {
718        return  newParent instanceof SitemapElement && ((SitemapElement) newParent).getSitemap().equals(getSitemap());
719    }
720
721    @Override
722    public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException
723    {
724        Node node = getNode();
725        try
726        {
727            node.getParent().orderBefore(node.getName(), siblingNode != null ? siblingNode.getName() : null);
728        }
729        catch (RepositoryException e)
730        {
731            throw new AmetysRepositoryException(String.format("Unable to order page '%s' before sibling '%s'", this, siblingNode != null ? siblingNode.getName() : ""), e);
732        }
733    }
734
735    @Override
736    public boolean isVisible() throws AmetysRepositoryException
737    {
738        try
739        {
740            if (getNode().hasProperty(METADATA_VISIBLE))
741            {
742                return getNode().getProperty(METADATA_VISIBLE).getBoolean();
743            }
744            else
745            {
746                return true;
747            }
748        }
749        catch (RepositoryException e)
750        {
751            throw new AmetysRepositoryException("Unable to get visible property", e);
752        }
753    }
754    
755    @Override
756    public void setVisible(boolean isVisible) throws AmetysRepositoryException
757    {
758        try
759        {
760            getNode().setProperty(METADATA_VISIBLE, isVisible);
761        }
762        catch (RepositoryException e)
763        {
764            throw new AmetysRepositoryException("Unable to set visible property", e);
765        }
766    }
767    
768    public boolean isIndexable()
769    {
770        try
771        {
772            if (getNode().hasProperty(METADATA_INDEXABLE))
773            {
774                return getNode().getProperty(METADATA_INDEXABLE).getBoolean();
775            }
776            else
777            {
778                return true;
779            }
780        }
781        catch (RepositoryException e)
782        {
783            throw new AmetysRepositoryException("Unable to get indexable property", e);
784        }
785    }
786    
787    @Override
788    public void setIndexable(boolean indexable) throws AmetysRepositoryException
789    {
790        try
791        {
792            getNode().setProperty(METADATA_INDEXABLE, indexable);
793        }
794        catch (RepositoryException e)
795        {
796            throw new AmetysRepositoryException("Unable to set indexable property", e);
797        }
798    }
799    
800    @Override
801    public Map<String, Object> dataToJSON(DataContext context) throws UnknownTypeException, NotUniqueTypeException, BadItemTypeException
802    {
803        Map<String, Object> result = new HashMap<>();
804        for (String dataName : getDataNames())
805        {
806            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
807            result.put(dataName, dataToJSON(dataName, newContext));
808        }
809        
810        return result;
811    }
812    
813    @Override
814    public Object dataToJSON(String dataPath, DataContext context) throws UnknownTypeException, NotUniqueTypeException, BadItemTypeException
815    {
816        // Do not convert view parameters
817        return !ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME.equals(dataPath)
818                ? super.dataToJSON(dataPath, context)
819                : null;
820    }
821
822    public boolean isLocked() throws AmetysRepositoryException
823    {
824        try
825        {
826            if (getNode().hasProperty(METADATA_LOCKED))
827            {
828                return getNode().getProperty(METADATA_LOCKED).getBoolean();
829            }
830            else
831            {
832                return false;
833            }
834        }
835        catch (RepositoryException e)
836        {
837            throw new AmetysRepositoryException("Unable to get locked property", e);
838        }
839    }
840
841    public void lock() throws AmetysRepositoryException
842    {
843        try
844        {
845            getNode().setProperty(METADATA_LOCKED, true);
846        }
847        catch (RepositoryException e)
848        {
849            throw new AmetysRepositoryException("Unable to set locked property", e);
850        }
851    }
852
853    public void unlock() throws AmetysRepositoryException
854    {
855        try
856        {
857            getNode().setProperty(METADATA_LOCKED, false);
858        }
859        catch (RepositoryException e)
860        {
861            throw new AmetysRepositoryException("Unable to set locked property", e);
862        }
863    }
864    
865    public TrashElement moveToTrash()
866    {
867        TrashElement trashElement = _getFactory()._getTrashElementDAO().createTrashElement(this, getTitle());
868        
869        // Clone the ametys object from the original session to trash session
870        Node storedNode = NodeHelper.cloneNode(getNode(), trashElement.getNode());
871        
872        try
873        {
874            storedNode.setProperty("ametys-internal:path", getParentPath());
875            // Update strong reference data to avoid NoSuchElementException
876            DefaultPage<?> page = trashElement.getChild(storedNode.getName());
877            page._updateContentReferences(this::_saveContentReference);
878        }
879        catch (RepositoryException e)
880        {
881            throw new AmetysRepositoryException("failed to store the content path", e);
882        }
883
884        // Remove the node from the original session
885        remove();
886        
887        return trashElement;
888    }
889    
890    public DefaultPage restoreFromTrash(MergeComputationMode mergeComputationMode) throws UnknownParentException
891    {
892        try
893        {
894            Property property = getNode().getProperty("ametys-internal:path");
895            String parentPath = property.getValue().getString();
896            JCRTraversableAmetysObject parent;
897            try
898            {
899                parent = _getFactory()._getAOResolver().resolveByPath(parentPath);
900            }
901            catch (UnknownAmetysObjectException e)
902            {
903                throw new UnknownParentException("No parent available at path " + parentPath, e);
904            }
905            
906            // remove trash data before restoration
907            property.remove();
908            
909            // Get the node name, can be adjust if another content has already the same node name
910            String nodeName = parent.hasChild(getName()) ? PageDAO.findFreePageNameByTitle(parent, getTitle()) : getName();
911            // Clone the ametys object from the trash session to default session
912            Node restoredNode = NodeHelper.cloneNode(getNode(), parent.getNode(), nodeName);
913            
914            // Update strong reference data to avoid NoSuchElementException
915            DefaultPage<?> restoredPage = parent.getChild(restoredNode.getName());
916            restoredPage._updateContentReferences(this::_restoreContentReference);
917            
918            // Remove the node from the trash session
919            remove();
920            
921            restoredPage.saveChanges();
922            
923            return restoredPage;
924        }
925        catch (RepositoryException e)
926        {
927            throw new AmetysRepositoryException("failed to retrieve the page path", e);
928        }
929    }
930    
931    private void _updateContentReferences(Consumer<DefaultZoneItem> updateConsumer)
932    {
933        // Get contents referencing into the pages
934        getZones().stream()
935            .map(Zone::getZoneItems)
936            .flatMap(AmetysObjectIterable::stream)
937            .filter(DefaultZoneItem.class::isInstance)
938            .filter(zi -> zi.getType() == ZoneItem.ZoneType.CONTENT)
939            .map(DefaultZoneItem.class::cast)
940            .forEach(updateConsumer);
941        
942        // Explore children pages
943        getChildrenPages().stream()
944            .filter(DefaultPage.class::isInstance)
945            .map(p -> (DefaultPage<?>) p)
946            .forEach(p -> p._updateContentReferences(updateConsumer));
947    }
948    
949    private void _saveContentReference(DefaultZoneItem zoneItem)
950    {
951        try
952        {
953            Node zoneItemNode = zoneItem.getNode();
954            if (zoneItemNode.hasProperty(DefaultZoneItem.METADATA_CONTENT))
955            {
956                Property oldProperty = zoneItemNode.getProperty(DefaultZoneItem.METADATA_CONTENT);
957                zoneItemNode.setProperty("ametys-internal:trash-content", oldProperty.getValue(), PropertyType.WEAKREFERENCE);
958                oldProperty.remove();
959            }
960            else
961            {
962                // If the content property is not there, zone item is obsolete, remove it
963                zoneItem.remove();
964            }
965        }
966        catch (RepositoryException e)
967        {
968            throw new AmetysRepositoryException(e);
969        }
970    }
971
972    private void _restoreContentReference(DefaultZoneItem zoneItem)
973    {
974        try
975        {
976            Node zoneItemNode = zoneItem.getNode();
977            Property oldProperty = zoneItemNode.getProperty("ametys-internal:trash-content");
978            try
979            {
980                // Check if the node is in the target session
981                oldProperty.getNode();
982                
983                // Update the reference
984                zoneItemNode.setProperty(DefaultZoneItem.METADATA_CONTENT, oldProperty.getValue(), PropertyType.REFERENCE);
985                oldProperty.remove();
986            }
987            catch (ItemNotFoundException e)
988            {
989                // The content is not in the target session, remove the zone item
990                zoneItem.remove();
991            }
992        }
993        catch (RepositoryException e)
994        {
995            throw new AmetysRepositoryException(e);
996        }
997    }
998}