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