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