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;
025
026import javax.jcr.ItemExistsException;
027import javax.jcr.Node;
028import javax.jcr.RepositoryException;
029import javax.jcr.Value;
030
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.plugins.explorer.resources.ResourceCollection;
034import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
035import org.ametys.plugins.repository.AmetysObject;
036import org.ametys.plugins.repository.AmetysObjectIterable;
037import org.ametys.plugins.repository.AmetysRepositoryException;
038import org.ametys.plugins.repository.CopiableAmetysObject;
039import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
040import org.ametys.plugins.repository.RepositoryConstants;
041import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
042import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
043import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
044import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
045import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
046import org.ametys.runtime.model.exception.BadItemTypeException;
047import org.ametys.runtime.model.exception.NotUniqueTypeException;
048import org.ametys.runtime.model.exception.UndefinedItemPathException;
049import org.ametys.runtime.model.exception.UnknownTypeException;
050import org.ametys.runtime.model.type.DataContext;
051import org.ametys.runtime.model.type.ModelItemTypeConstants;
052import org.ametys.web.parameters.view.ViewParametersManager;
053import org.ametys.web.repository.page.ModifiablePage;
054import org.ametys.web.repository.page.ModifiableZone;
055import org.ametys.web.repository.page.MoveablePage;
056import org.ametys.web.repository.page.Page;
057import org.ametys.web.repository.page.PageDAO;
058import org.ametys.web.repository.page.SitemapElement;
059import org.ametys.web.repository.site.Site;
060import org.ametys.web.repository.sitemap.Sitemap;
061
062/**
063 * {@link Page} implementation stored into a JCR node using
064 * <code>ametys:defaultPage</code> node type.
065 * 
066 * @param <F> the actual type of factory.
067 */
068public class DefaultPage<F extends DefaultPageFactory> extends AbstractSitemapElement<F> implements ModifiablePage, CopiableAmetysObject, MoveablePage
069{
070    /** Constant for template metadata. */
071    public static final String METADATA_TEMPLATE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":template";
072
073    /** Constant for title metadata. */
074    public static final String METADATA_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title";
075
076    /** Constant for the initial title metadata. */
077    public static final String METADATA_INITIAL_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title-initial";
078
079    /** Constant for long title metadata. */
080    public static final String METADATA_LONG_TITLE = "long-title";
081
082    /** Constant for title metadata. */
083    public static final String METADATA_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":type";
084
085    /** Constant for title metadata. */
086    public static final String METADATA_URL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url";
087
088    /** Constant for title metadata. */
089    public static final String METADATA_URLTYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url-type";
090
091    /** Constant for referers metadata. */
092    public static final String METADATA_REFERERS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":referers";
093    
094    /** Constant for the visible attribute. */
095    public static final String METADATA_VISIBLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":visible";
096
097    /** Constant for robots metadata. */
098    public static final String METADATA_ROBOTS_DISALLOW = "robots-disallow";
099    
100    /** Constant for the attachment node name. */
101    public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments";
102    
103    /** Constants for sitemap Metadata* */
104    public static final String METADATA_SITEMAP = "sitemap";
105
106    /** Constant for publication start date metadata. */
107    public static final String METADATA_PUBLICATION_START_DATE = "publicationStartDate";
108    
109    /** Constant for publication end date metadata. */
110    public static final String METADATA_PUBLICATION_END_DATE = "publicationEndDate";
111    
112    /**
113     * Creates a {@link DefaultPage}.
114     * 
115     * @param node the node backing this {@link AmetysObject}.
116     * @param parentPath the parent path in the Ametys hierarchy.
117     * @param factory the {@link DefaultPageFactory} which creates the
118     *            AmetysObject.
119     */
120    public DefaultPage(Node node, String parentPath, F factory)
121    {
122        super(node, parentPath, factory);
123    }
124    
125    @Override
126    public String getTemplate() throws AmetysRepositoryException
127    {
128        if (getType() != PageType.CONTAINER)
129        {
130            return null;
131        }
132        
133        try
134        {
135            Node node = getNode();
136            
137            if (node.hasProperty(METADATA_TEMPLATE))
138            {
139                return node.getProperty(METADATA_TEMPLATE).getString();
140            }
141            else
142            {
143                return null;
144            }
145        }
146        catch (RepositoryException e)
147        {
148            throw new AmetysRepositoryException("Unable to get template property", e);
149        }
150    }
151
152    @Override
153    public void setTemplate(String template) throws AmetysRepositoryException
154    {
155        try
156        {
157            getNode().setProperty(METADATA_TEMPLATE, template);
158        }
159        catch (RepositoryException e)
160        {
161            throw new AmetysRepositoryException("Unable to set template property", e);
162        }
163    }
164
165    @Override
166    public Set<String> getTags() throws AmetysRepositoryException
167    {
168        return TaggableAmetysObjectHelper.getTags(this);
169    }
170    
171    @Override
172    public void tag(String tag) throws AmetysRepositoryException
173    {
174        TaggableAmetysObjectHelper.tag(this, tag);
175    }
176
177    @Override
178    public void untag(String tag) throws AmetysRepositoryException
179    {
180        TaggableAmetysObjectHelper.untag(this, tag);
181    }
182
183    @Override
184    public String getTitle() throws AmetysRepositoryException
185    {
186        try
187        {
188            return getNode().getProperty(METADATA_TITLE).getString();
189        }
190        catch (RepositoryException e)
191        {
192            throw new AmetysRepositoryException("Unable to get title property", e);
193        }
194    }
195    
196    @Override
197    public void setTitle(String title) throws AmetysRepositoryException
198    {
199        try
200        {
201            getNode().setProperty(METADATA_TITLE, title);
202        }
203        catch (RepositoryException e)
204        {
205            throw new AmetysRepositoryException("Unable to set title property", e);
206        }
207        setInitialTitle(title);
208    }
209
210    /**
211     * Get the title that was set by the user (and not set by a copy adding a number after)
212     * @return The initial title
213     * @throws AmetysRepositoryException if an error occurs
214     */
215    public String getInitialTitle()  throws AmetysRepositoryException
216    {
217        try
218        {
219            if (getNode().hasProperty(METADATA_INITIAL_TITLE))
220            {
221                return getNode().getProperty(METADATA_INITIAL_TITLE).getString();
222            }
223            else
224            {
225                return getTitle();
226            }
227        }
228        catch (RepositoryException e)
229        {
230            throw new AmetysRepositoryException("Unable to get initial title property", e);
231        }
232    }
233    
234    /**
235     * Set the title that was set by the user (and not set by a copy adding a number after)
236     * @param initialTitle The initial title to set
237     * @throws AmetysRepositoryException if an error occurs
238     */
239    public void setInitialTitle(String initialTitle) throws AmetysRepositoryException
240    {
241        try
242        {
243            getNode().setProperty(METADATA_INITIAL_TITLE, initialTitle);
244        }
245        catch (RepositoryException e)
246        {
247            throw new AmetysRepositoryException("Unable to set initial title property", e);
248        }
249    }
250
251    @Override
252    public String getLongTitle() throws AmetysRepositoryException
253    {
254        String longTitle = getValue(METADATA_LONG_TITLE);
255        if (StringUtils.isNotBlank(longTitle))
256        {
257            return longTitle;
258        }
259        else
260        {
261            return this.getTitle();
262        }
263    }
264
265    @Override
266    public void setLongTitle(String title) throws AmetysRepositoryException
267    {
268        String newTitle = StringUtils.trimToNull(title);
269        setValue(METADATA_LONG_TITLE, newTitle, ModelItemTypeConstants.STRING_TYPE_ID);
270    }
271
272    @Override
273    public Sitemap getSitemap() throws AmetysRepositoryException
274    {
275        try
276        {
277            Node node = getNode();
278
279            do
280            {
281                node = node.getParent();
282            }
283            while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE));
284
285            return _getFactory().resolveAmetysObject(node);
286        }
287        catch (RepositoryException e)
288        {
289            throw new AmetysRepositoryException("Unable to get sitemap", e);
290        }
291    }
292    
293    @Override
294    public int getDepth() throws AmetysRepositoryException
295    {
296        try
297        {
298            int depth = 1;
299            Node node = getNode().getParent();
300            while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE))
301            {
302                depth++;
303                node = node.getParent();
304            }
305
306            return depth;
307        }
308        catch (RepositoryException e)
309        {
310            throw new AmetysRepositoryException("Unable to get depth", e);
311        }
312    }
313
314    @Override
315    public Site getSite() throws AmetysRepositoryException
316    {
317        return getSitemap().getSite();
318    }
319    
320    @Override
321    public String getSiteName()
322    {
323        return getValue(METADATA_SITE);
324    }
325    
326    @Override
327    public void setSiteName(String siteName)
328    {
329        setValue(METADATA_SITE, siteName);
330    }
331    
332    @Override
333    public String getSitemapName() throws AmetysRepositoryException
334    {
335        return getValue(METADATA_SITEMAP);
336    }
337    
338    @Override
339    public void setSitemapName(String sitemapName) throws AmetysRepositoryException
340    {
341        setValue(METADATA_SITEMAP, sitemapName);
342    }
343
344    @Override
345    public String getPathInSitemap() throws AmetysRepositoryException
346    {
347        String nodePath = getPath();
348        String sitemapPath = getSitemap().getPath();
349        return nodePath.substring(sitemapPath.length() + 1);
350    }
351
352    @Override
353    public PageType getType() throws AmetysRepositoryException
354    {
355        try
356        {
357            return PageType.valueOf(getNode().getProperty(METADATA_TYPE).getString());
358        }
359        catch (RepositoryException e)
360        {
361            throw new AmetysRepositoryException("Unable to get type property", e);
362        }
363    }
364
365    @Override
366    public void setType(PageType type) throws AmetysRepositoryException
367    {
368        try
369        {
370            getNode().setProperty(METADATA_TYPE, type.name());
371        }
372        catch (RepositoryException e)
373        {
374            throw new AmetysRepositoryException("Unable to set type property", e);
375        }
376    }
377
378    @Override
379    public String getURL() throws AmetysRepositoryException
380    {
381        try
382        {
383            return getNode().getProperty(METADATA_URL).getString();
384        }
385        catch (RepositoryException e)
386        {
387            throw new AmetysRepositoryException("Unable to get url property", e);
388        }
389    }
390    
391    @Override
392    public LinkType getURLType() throws AmetysRepositoryException
393    {
394        try
395        {
396            return LinkType.valueOf(getNode().getProperty(METADATA_URLTYPE).getString());
397        }
398        catch (RepositoryException e)
399        {
400            throw new AmetysRepositoryException("Unable to get url type property", e);
401        }
402    }
403
404    @Override
405    public void setURL(LinkType type, String url) throws AmetysRepositoryException
406    {
407        try
408        {
409            getNode().setProperty(METADATA_URLTYPE, type.toString());
410            getNode().setProperty(METADATA_URL, url);
411        }
412        catch (RepositoryException e)
413        {
414            throw new AmetysRepositoryException("Unable to set url property", e);
415        }
416    }
417
418    @Override
419    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
420    {
421        ResourceCollection attachments;
422        if (hasChild(ATTACHMENTS_NODE_NAME))
423        {
424            attachments = getChild(ATTACHMENTS_NODE_NAME);
425        }
426        else
427        {
428            attachments = createChild(ATTACHMENTS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE);
429            
430            try
431            {
432                getNode().getSession().save();
433            }
434            catch (RepositoryException e)
435            {
436                throw new AmetysRepositoryException("Unable to save session", e);
437            }
438        }
439        
440        return attachments;
441    }
442    
443    @Override
444    public Set<String> getReferers() throws AmetysRepositoryException
445    {
446        try
447        {
448            Node node = getNode();
449
450            if (!node.hasProperty(METADATA_REFERERS))
451            {
452                return Collections.emptySet();
453            }
454
455            Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
456            Set<String> ids = new HashSet<>(values.length);
457
458            for (Value value : values)
459            {
460                ids.add(value.getString());
461            }
462
463            return ids;
464        }
465        catch (RepositoryException e)
466        {
467            throw new AmetysRepositoryException("Unable to get referers property", e);
468        }
469    }
470
471    @Override
472    public void addReferer(String ametysObjectId) throws AmetysRepositoryException
473    {
474        try
475        {
476            Set<String> ids = null;
477            Node node = getNode();
478
479            if (!node.hasProperty(METADATA_REFERERS))
480            {
481                ids = Collections.singleton(ametysObjectId);
482            }
483            else
484            {
485                Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
486                ids = new HashSet<>(values.length + 1);
487
488                for (Value value : values)
489                {
490                    ids.add(value.getString());
491                }
492
493                ids.add(ametysObjectId);
494            }
495
496            node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()]));
497        }
498        catch (RepositoryException e)
499        {
500            throw new AmetysRepositoryException("Unable to add an id into referers property", e);
501        }
502    }
503
504    @Override
505    public void removeReferer(String ametysObjectId) throws AmetysRepositoryException
506    {
507        try
508        {
509            Node node = getNode();
510
511            if (node.hasProperty(METADATA_REFERERS))
512            {
513                Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
514                Set<String> ids = new HashSet<>(values.length + 1);
515
516                for (Value value : values)
517                {
518                    ids.add(value.getString());
519                }
520
521                ids.remove(ametysObjectId);
522                node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()]));
523            }
524        }
525        catch (RepositoryException e)
526        {
527            throw new AmetysRepositoryException("Unable to remove an id from referers property", e);
528        }
529    }
530    
531    @Override
532    public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
533    {
534        try
535        {
536            String pageName = PageDAO.findFreePageName(parent, name != null ? name : getName());
537            
538            DefaultPage cPage = parent.createChild(pageName, "ametys:defaultPage");
539            cPage.setType(getType());
540            cPage.setTitle(getTitle());
541            cPage.setInitialTitle(getInitialTitle());
542            
543            if (PageType.CONTAINER.equals(getType()))
544            {
545                cPage.setTemplate(getTemplate());
546            }
547            else if (PageType.LINK.equals(getType()))
548            {
549                cPage.setURL(getURLType(), getURL());
550            }
551            
552            // Copy metadata
553            copyTo(cPage);
554            
555            // Copy view parameters
556            getTemplateParametersHolder().copyTo(cPage.getTemplateParametersHolder());
557            
558            // Copy zones
559            for (ModifiableZone zone : getZones())
560            {
561                if (zone instanceof CopiableAmetysObject)
562                {
563                    ((CopiableAmetysObject) zone).copyTo(cPage, null);
564                }
565            }
566            
567            // Copy tags
568            Set<String> tags = getTags();
569            for (String tag : tags)
570            {
571                cPage.tag(tag);
572            }
573            
574            // Update sitemap name
575            if (parent instanceof SitemapElement)
576            {
577                cPage.setSitemapName(((SitemapElement) parent).getSitemapName());
578            }
579            
580            parent.saveChanges();
581            
582            // Copy attachments
583            ResourceCollection rootAttachments = getRootAttachments();
584            if (rootAttachments instanceof SimpleAmetysObject)
585            {
586                Node resourcesNode = ((SimpleAmetysObject) rootAttachments).getNode();
587                getNode().getSession().getWorkspace().copy(resourcesNode.getPath(), cPage.getNode().getPath() + "/" + resourcesNode.getName());
588            }
589            
590            // Copy sub-pages
591            AmetysObjectIterable< ? extends Page> childrenPages = getChildrenPages();
592            for (Page page : childrenPages)
593            {
594                // Avoid infinite loop : do not copy the page itself
595                if (page instanceof CopiableAmetysObject && restrictTo.contains(page.getId()))
596                {
597                    ((CopiableAmetysObject) page).copyTo(cPage, null, restrictTo);
598                }
599            }
600            
601            return cPage;
602        }
603        catch (RepositoryException e)
604        {
605            throw new AmetysRepositoryException(e);
606        }
607    }
608    
609    @Override
610    public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
611    {
612        return copyTo (parent, name, new ArrayList<>());
613    }
614    
615    public void copyTo(ModifiableDataHolder dataHolder) throws UndefinedItemPathException, BadItemTypeException, UnknownTypeException, NotUniqueTypeException
616    {
617        for (String name : getDataNames())
618        {
619            if (!_getExcludedDataFromCopy().contains(name))
620            {
621                DataHolderHelper.copyTo(getDataHolder(), dataHolder, name, DataContext.newInstance());
622            }
623        }
624    }
625    
626    /**
627     * Get the set of data name to exclude from the copy
628     * @return the set of data name to exclude from the copy
629     */
630    protected Set<String> _getExcludedDataFromCopy()
631    {
632        return Set.of(
633            ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME,
634            METADATA_PUBLICATION_START_DATE,
635            METADATA_PUBLICATION_END_DATE
636        );
637    }
638    
639    @Override
640    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
641    {
642        Node node = getNode();
643
644        try
645        {
646            if (getParent().equals(newParent))
647            {
648                // Just order current node to the end
649                node.getParent().orderBefore(node.getName(), null);
650            }
651            else
652            {
653                if (!canMoveTo(newParent))
654                {
655                    throw new AmetysRepositoryException("DefaultPage " + toString() + " can only be moved to a CompositePages and ");
656                }
657                SitemapElement newParentPage = (SitemapElement) newParent;
658                
659                String sitemapNodePath = getSitemap().getNode().getPath();
660                String newPath = sitemapNodePath + (newParentPage instanceof Sitemap ? "" : '/' + newParentPage.getPathInSitemap());
661
662                String name = node.getName();
663                
664                if (renameIfExist)
665                {
666                    // Find unused name on new parent node
667                    int index = 1;
668                    while (node.getSession().getRootNode().hasNode(newPath.substring(1) + "/" + name))
669                    {
670                        name = name + "-" + index++;
671                    }
672                }
673
674                try
675                {
676                    // Move node
677                    node.getSession().move(node.getPath(), newPath + "/" + name);
678                }
679                catch (ItemExistsException e)
680                {
681                    throw new AmetysRepositoryException(String.format("A page already exists for in parent path '%s'", newPath), e);
682                }
683                
684                // recompute name in case it has changed
685                _invalidateName();
686
687                // Invalidate parent path as the parent path has changed
688                _invalidateParentPath();
689            }
690        }
691        catch (RepositoryException e)
692        {
693            throw new AmetysRepositoryException(String.format("Unable to move page '%s' to node '%s'", this, newParent.getId()), e);
694        }
695    }
696    
697    @Override
698    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
699    {
700        return  newParent instanceof SitemapElement && ((SitemapElement) newParent).getSitemap().equals(getSitemap());
701    }
702
703    @Override
704    public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException
705    {
706        Node node = getNode();
707        try
708        {
709            node.getParent().orderBefore(node.getName(), siblingNode != null ? siblingNode.getName() : null);
710        }
711        catch (RepositoryException e)
712        {
713            throw new AmetysRepositoryException(String.format("Unable to order page '%s' before sibling '%s'", this, siblingNode != null ? siblingNode.getName() : ""), e);
714        }
715    }
716
717    @Override
718    public boolean isVisible() throws AmetysRepositoryException
719    {
720        try
721        {
722            if (getNode().hasProperty(METADATA_VISIBLE))
723            {
724                return getNode().getProperty(METADATA_VISIBLE).getBoolean();
725            }
726            else
727            {
728                return true;
729            }
730        }
731        catch (RepositoryException e)
732        {
733            throw new AmetysRepositoryException("Unable to get visible property", e);
734        }
735    }
736    
737    @Override
738    public void setVisible(boolean isVisible) throws AmetysRepositoryException
739    {
740        try
741        {
742            getNode().setProperty(METADATA_VISIBLE, isVisible);
743        }
744        catch (RepositoryException e)
745        {
746            throw new AmetysRepositoryException("Unable to set visible property", e);
747        }
748    }
749    
750    public Map<String, Object> dataToJSON(DataContext context) throws UnknownTypeException, NotUniqueTypeException
751    {
752        Map<String, Object> result = new HashMap<>();
753        for (String dataName : getDataNames())
754        {
755            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
756            result.put(dataName, dataToJSON(dataName, newContext));
757        }
758        
759        return result;
760    }
761    
762    public Object dataToJSON(String dataPath, DataContext context)
763    {
764        // Do not convert view parameters
765        return !ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME.equals(dataPath)
766                ? ModifiablePage.super.dataToJSON(dataPath, context)
767                : null;
768    }
769}