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.site;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.List;
021
022import javax.jcr.ItemExistsException;
023import javax.jcr.Node;
024import javax.jcr.NodeIterator;
025import javax.jcr.Property;
026import javax.jcr.PropertyIterator;
027import javax.jcr.RepositoryException;
028
029import org.apache.cocoon.util.HashUtil;
030import org.apache.commons.lang.StringUtils;
031
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.repository.ModifiableContent;
034import org.ametys.plugins.repository.AmetysObject;
035import org.ametys.plugins.repository.AmetysObjectIterable;
036import org.ametys.plugins.repository.AmetysRepositoryException;
037import org.ametys.plugins.repository.CollectionIterable;
038import org.ametys.plugins.repository.CopiableAmetysObject;
039import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
040import org.ametys.plugins.repository.MovableAmetysObject;
041import org.ametys.plugins.repository.RepositoryConstants;
042import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
043import org.ametys.plugins.repository.TraversableAmetysObject;
044import org.ametys.plugins.repository.UnknownAmetysObjectException;
045import org.ametys.plugins.repository.collection.AmetysObjectCollection;
046import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory;
047import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
048import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
049import org.ametys.plugins.repository.metadata.UnknownMetadataException;
050import org.ametys.web.pageaccess.RestrictedPagePolicy;
051import org.ametys.web.repository.sitemap.Sitemap;
052
053/**
054 * {@link AmetysObject} for storing site informations.
055 */
056public final class Site extends DefaultTraversableAmetysObject<SiteFactory> implements MovableAmetysObject, CopiableAmetysObject
057{
058    /** Site node type name. */
059    public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":site";
060    
061    /** Constants for title metadata. */
062    private static final String __METADATA_TITLE = "title";
063    /** Constants for type metadata. */
064    private static final String __METADATA_TYPE = "type";
065    /** Constants for description metadata. */
066    private static final String __METADATA_DESCRIPTION = "description";
067    /** Constants for URL metadata. */
068    private static final String __METADATA_URL = "url";
069    /** Constants for color metadata. */
070    private static final String __METADATA_COLOR = "color";
071    /** Constants for skin metadata. */
072    private static final String __METADATA_SKIN = "skin";
073    /** Constants for restricted page policy metadata. */
074    private static final String __METADATA_DISPLAY_RESTRICTED_PAGES = "display-restricted-pages";
075    /** Constants for sitemaps node name. */
076    private static final String __SITEMAPS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps";
077    /** Constants for contents node name. */
078    private static final String __CONTENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents";
079    /** Constants for resources node name. */
080    private static final String __RESOURCES_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources";
081    /** Constants for plugins node name. */
082    private static final String __PLUGINS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins";
083
084    /**
085     * Creates a {@link Site}.
086     * @param node the node backing this {@link AmetysObject}.
087     * @param parentPath the parent path in the Ametys hierarchy.
088     * @param factory the {@link SiteFactory} which creates the AmetysObject.
089     */
090    public Site(Node node, String parentPath, SiteFactory factory)
091    {
092        super(node, parentPath, factory);
093    }
094    
095    /**
096     * Retrieves the title.
097     * @return the title.
098     * @throws AmetysRepositoryException if an error occurs.
099     */
100    public String getTitle() throws AmetysRepositoryException
101    {
102        try
103        {
104            return getMetadataHolder().getString(__METADATA_TITLE);
105        }
106        catch (UnknownMetadataException e)
107        {
108            return null;
109        }
110    }
111    
112    /**
113     * Retrieves the type.
114     * @return the type.
115     * @throws AmetysRepositoryException if an error occurs.
116     */
117    public String getType() throws AmetysRepositoryException
118    {
119        // return default site type for null metadata for 3.0 compatibility
120        return getMetadataHolder().getString(__METADATA_TYPE, SiteType.DEFAULT_SITE_TYPE_ID);
121    }
122    
123
124    /**
125     * Set the type.
126     * @param type the type.
127     * @throws AmetysRepositoryException if an error occurs.
128     */
129    public void setType(String type) throws AmetysRepositoryException
130    {
131        getMetadataHolder().setMetadata(__METADATA_TYPE, type);
132    }
133    
134    /**
135     * Set the title.
136     * @param title the title.
137     * @throws AmetysRepositoryException if an error occurs.
138     */
139    public void setTitle(String title) throws AmetysRepositoryException
140    {
141        getMetadataHolder().setMetadata(__METADATA_TITLE, title);
142    }
143    
144    /**
145     * Retrieves the description.
146     * @return the description.
147     * @throws AmetysRepositoryException if an error occurs.
148     */
149    public String getDescription() throws AmetysRepositoryException
150    {
151        try
152        {
153            return getMetadataHolder().getString(__METADATA_DESCRIPTION);
154        }
155        catch (UnknownMetadataException e)
156        {
157            return null;
158        }
159    }
160    
161    /**
162     * Set the description.
163     * @param description the description.
164     * @throws AmetysRepositoryException if an error occurs.
165     */
166    public void setDescription(String description) throws AmetysRepositoryException
167    {
168        getMetadataHolder().setMetadata(__METADATA_DESCRIPTION, description);
169    }
170    
171    /**
172     * Retrieves the main url.
173     * @return the main url.
174     * @throws AmetysRepositoryException if an error occurs.
175     */
176    public String getUrl() throws AmetysRepositoryException
177    {
178        String[] aliases = getUrlAliases();
179        
180        return aliases != null && aliases.length > 0 ? aliases[0] : null;
181    }
182    
183    /**
184     * Retrieves the url aliases.
185     * @return the url.
186     * @throws AmetysRepositoryException if an error occurs.
187     */
188    public String[] getUrlAliases() throws AmetysRepositoryException
189    {
190        try
191        {
192            String url = getMetadataHolder().getString(__METADATA_URL, "");
193            String[] aliases = StringUtils.split(url, ',');
194            
195            for (int i = 0; i < aliases.length; i++)
196            {
197                aliases[i] = aliases[i].trim();
198            }
199            
200            return aliases;
201        }
202        catch (UnknownMetadataException e)
203        {
204            return null;
205        }
206    }
207    
208    /**
209     * Set the url.
210     * @param url the url.
211     * @throws AmetysRepositoryException if an error occurs.
212     */
213    public void setUrl(String url) throws AmetysRepositoryException
214    {
215        getMetadataHolder().setMetadata(__METADATA_URL, url);
216    }
217    
218    /**
219     * Retrieves the color
220     * @return The color.
221     * @throws AmetysRepositoryException if an error occurs.
222     */
223    public String getColor()  throws AmetysRepositoryException
224    {
225        return getMetadataHolder().getString(__METADATA_COLOR, "");
226    }
227
228    
229    /**
230     * Retrieves a sitemap.
231     * @param sitemapName the sitemap name.
232     * @return the sitemap found.
233     * @throws AmetysRepositoryException if an error occurs.
234     * @throws UnknownAmetysObjectException if the object does not exist.
235     */
236    public Sitemap getSitemap(String sitemapName) throws AmetysRepositoryException, UnknownAmetysObjectException
237    {
238        return ((TraversableAmetysObject) getChild(__SITEMAPS_NODE_NAME)).getChild(sitemapName);
239    }
240    
241    /**
242     * Determines the existence of a sitemap
243     * @param lang The sitemap language
244     * @return true if the sitemap exists
245     * @throws AmetysRepositoryException if an error occurred
246     */
247    public boolean hasSitemap (String lang) throws AmetysRepositoryException
248    {
249        DefaultTraversableAmetysObject<?> sitemaps = getChild(__SITEMAPS_NODE_NAME);
250        return sitemaps.hasChild(lang);
251    }
252    
253    /**
254     * Add a sitemap language to the site
255     * @param lang The sitemap language
256     * @return The created sitemap
257     * @throws AmetysRepositoryException if an error occurred or if the sitemap already exists
258     */
259    public Sitemap addSitemap (String lang) throws AmetysRepositoryException
260    {
261        DefaultTraversableAmetysObject<?> sitemaps = getChild(__SITEMAPS_NODE_NAME);
262        if (sitemaps.hasChild(lang))
263        {
264            throw new AmetysRepositoryException ("The sitemap '" + lang + "' already exists");
265        }
266        return sitemaps.createChild(lang, "ametys:sitemap");
267    }
268
269    /**
270     * Retrieves sitemaps.
271     * @return the sitemaps or an empty {@link AmetysObjectIterable}.
272     * @throws AmetysRepositoryException if an error occurs.
273     */
274    public AmetysObjectIterable<Sitemap> getSitemaps() throws AmetysRepositoryException
275    {
276        return ((TraversableAmetysObject) getChild(__SITEMAPS_NODE_NAME)).getChildren();
277    }
278    
279    /**
280     * Retrieves root contents.
281     * @return the root for contents
282     * @throws AmetysRepositoryException if an error occurs.
283     */
284    public ModifiableTraversableAmetysObject getRootContents() throws AmetysRepositoryException
285    {
286        return getChild(__CONTENTS_NODE_NAME);
287    }
288    
289    /**
290     * Retrieves contents.
291     * @return the contents or an empty {@link AmetysObjectIterable}.
292     * @throws AmetysRepositoryException if an error occurs.
293     */
294    public AmetysObjectIterable<Content> getContents() throws AmetysRepositoryException
295    {
296        return ((TraversableAmetysObject) getChild(__CONTENTS_NODE_NAME)).getChildren();
297    }
298    
299    /**
300     * Retrieves root resources.
301     * @return the root for resources
302     * @throws AmetysRepositoryException if an error occurs.
303     */
304    public ModifiableTraversableAmetysObject getRootResources() throws AmetysRepositoryException
305    {
306        return getChild(__RESOURCES_NODE_NAME);
307    }
308    
309    /**
310     * Retrieves resources.
311     * @return the resources or an empty {@link AmetysObjectIterable}.
312     * @throws AmetysRepositoryException if an error occurs.
313     */
314    public AmetysObjectIterable<AmetysObject> getResources() throws AmetysRepositoryException
315    {
316        return ((TraversableAmetysObject) getChild(__RESOURCES_NODE_NAME)).getChildren();
317    }
318    
319    /**
320     * Get the root for plugins
321     * @return the root for plugins
322     * @throws AmetysRepositoryException if an error occurs.
323     */
324    public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException
325    {
326        return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME);
327    }
328    
329    /**
330     * Set the skin id for this site.
331     * @param skinId ths skin id
332     */
333    public void setSkinId(String skinId)
334    {
335        getMetadataHolder().setMetadata(__METADATA_SKIN, skinId);
336    }
337    
338    /**
339     * Returns the skin id for this site.
340     * @return the skin id for this site.
341     */
342    public String getSkinId()
343    {
344        return getMetadataHolder().getString(__METADATA_SKIN);
345    }
346    
347    /**
348     * Returns the parent site, if any.
349     * @return the parent site, if any.
350     */
351    public Site getParentSite()
352    {
353        AmetysObject parent = getParent();
354        if (SiteManager.ROOT_SITES_PATH.equals(parent.getPath()))
355        {
356            return null;
357        }
358        return parent.getParent();
359    }
360    
361    /**
362     * Returns the site path
363     * @return the site path
364     */
365    public String getSitePath ()
366    {
367        String path = getName();
368        
369        Site parentSite = getParentSite();
370        while (parentSite != null)
371        {
372            path = parentSite.getName() + "/" + path;
373            parentSite = parentSite.getParentSite();
374        }
375        
376        return path;
377    }
378    
379    /**
380     * Returns the {@link RestrictedPagePolicy} associated with this Site.
381     * @return the {@link RestrictedPagePolicy} associated with this Site.
382     */
383    public RestrictedPagePolicy getRestrictedPagePolicy()
384    {
385        boolean displayRestrictedPages = getMetadataHolder().getBoolean(__METADATA_DISPLAY_RESTRICTED_PAGES, true);
386        
387        if (displayRestrictedPages)
388        {
389            return RestrictedPagePolicy.DISPLAYED;
390        }
391        else
392        {
393            return RestrictedPagePolicy.HIDDEN;
394        }
395    }
396    
397    /**
398     * Returns the named {@link Site}.
399     * @param siteName the site name.
400     * @return the named {@link Site}.
401     * @throws UnknownAmetysObjectException if the named site does not exist.
402     */
403    public Site getSite (String siteName) throws UnknownAmetysObjectException
404    {
405        DefaultTraversableAmetysObject root = getChild(SiteManager.ROOT_SITES);
406        return (Site) root.getChild(siteName);
407    }
408    
409    /**
410     * Return true if the given site if an ancestor
411     * @param siteName the site name.
412     * @return  true if the given site if an ancestor
413     */
414    public boolean hasAncestor (String siteName)
415    {
416        // Is a parent ?
417        Site parentSite = getParentSite();
418        while (parentSite != null)
419        {
420            if (parentSite.getName().equals(siteName))
421            {
422                return true;
423            }
424            parentSite = parentSite.getParentSite();
425        }
426        
427        return false;
428    }
429    
430    /**
431     * Return true if the given site if a descendant
432     * @param siteName the site name.
433     * @return  true if the given site if a descendant
434     */
435    public boolean hasDescendant (String siteName)
436    {
437        if (hasChildrenSite(siteName))
438        {
439            return true;
440        }
441        
442        AmetysObjectIterable<Site> childrenSites = getChildrenSites();
443        for (Site child : childrenSites)
444        {
445            if (child.hasDescendant (siteName))
446            {
447                return true;
448            }
449        }
450        
451        return false;
452    }
453    
454    /**
455     * Returns true if the given site exists.
456     * @param siteName the site name.
457     * @return true if the given site exists.
458     */
459    public boolean hasChildrenSite(String siteName)
460    {
461        try
462        {
463            DefaultTraversableAmetysObject root = getChild(SiteManager.ROOT_SITES);
464            return root.hasChild(siteName);
465        }
466        catch (UnknownAmetysObjectException e)
467        {
468            return false;
469        }
470    }
471    
472    /**
473     * Returns the sites names.
474     * @return the sites names.
475     */
476    public Collection<String> getChildrenSiteNames()
477    {
478        AmetysObjectIterable<Site> sites = getChildrenSites();
479        
480        ArrayList<String> result = new ArrayList<>();
481        
482        for (Site site : sites)
483        {
484            result.add(site.getName());
485        }
486        
487        return result;
488    }
489    
490    /**
491     * Returns all children sites, or empty if none.
492     * @return all children sites, or empty if none.
493     * @throws AmetysRepositoryException if an error occurs
494     */
495    public AmetysObjectIterable<Site> getChildrenSites() throws AmetysRepositoryException
496    {
497        try
498        {
499            TraversableAmetysObject rootSites = getChild(SiteManager.ROOT_SITES);
500            return rootSites.getChildren();
501        }
502        catch (UnknownAmetysObjectException e)
503        {
504            return new CollectionIterable<>(new ArrayList<Site>());
505        }
506    }
507    
508    @Override
509    public void remove() throws AmetysRepositoryException, RepositoryIntegrityViolationException
510    {
511        for (Content content : getContents())
512        {
513            // FIXME API check if not modifiable
514            if (content instanceof ModifiableContent)
515            {
516                ((ModifiableContent) content).remove();
517            }
518        }
519        
520        for (Site site : getChildrenSites())
521        {
522            site.remove();
523        }
524            
525        super.remove();
526    }
527    
528    @Override
529    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
530    {
531        return newParent instanceof Site || newParent instanceof AmetysObjectCollection;
532    }
533    
534    @Override
535    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
536    {
537        Node node = getNode();
538
539        try
540        {
541            if (getParent().equals(newParent))
542            {
543                // Do nothing
544            }
545            else
546            {
547                if (!canMoveTo(newParent))
548                {
549                    throw new AmetysRepositoryException("Site " + toString() + " can only be moved to a site or the root of site");
550                }
551                
552                String name = node.getName();
553                
554                try
555                {
556                    // Move node
557                    if (newParent instanceof Site)
558                    {
559                        Site parentSite = (Site) newParent;
560                        if (!parentSite.hasChild("ametys-internal:sites"))
561                        {
562                            parentSite.createChild("ametys-internal:sites", "ametys:sites");
563                        }
564                        node.getSession().move(node.getPath(), parentSite.getNode().getPath() + "/ametys-internal:sites/" + name);
565                    }
566                    else
567                    {
568                        Node contextNode = ((AmetysObjectCollection) newParent).getNode();
569                        String[] hash = _getHashedPath (name);
570                        for (String hashPart : hash)
571                        {
572                            if (!contextNode.hasNode(hashPart))
573                            {
574                                contextNode = contextNode.addNode(hashPart, AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE);
575                            }
576                            else
577                            {
578                                contextNode = contextNode.getNode(hashPart); 
579                            }
580                        }
581                        node.getSession().move(node.getPath(), contextNode.getPath() + "/" + name);
582                    }
583                    
584                }
585                catch (ItemExistsException e)
586                {
587                    throw new AmetysRepositoryException(String.format("A site already exists for in parent path '%s'", newParent.getPath() + "/" + name), e);
588                }
589                
590                // Invalidate parent path as the parent path has changed
591                _invalidateParentPath();
592            }
593        }
594        catch (RepositoryException e)
595        {
596            throw new AmetysRepositoryException(String.format("Unable to move site '%s' to node '%s'", this, newParent.getId()), e);
597        }
598    }
599    
600    @Override
601    public Site copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
602    {
603        try
604        {
605            Node parentNode = _getParentNode(parent, name);
606            Node clonedSite = parentNode.addNode(name, "ametys:site");
607                        
608            parent.saveChanges();
609            
610            // Copy properties
611            PropertyIterator properties = getNode().getProperties("ametys:*");
612            while (properties.hasNext())
613            {
614                Property property = (Property) properties.next();
615                
616                if (property.getDefinition().isMultiple())
617                {
618                    clonedSite.setProperty(property.getName(), property.getValues());
619                }
620                else
621                {
622                    clonedSite.setProperty(property.getName(), property.getValue());
623                }
624            }
625            
626            // Copy resources
627            Node resourcesNode = getNode().getNode("ametys-internal:resources");
628            getNode().getSession().getWorkspace().copy(resourcesNode.getPath(), clonedSite.getPath() + "/" + "ametys-internal:resources");
629            
630            // Copy plugins (ametys-internal:plugins is auto-created)
631            NodeIterator plugins = getNode().getNode("ametys-internal:plugins").getNodes();
632            while (plugins.hasNext())
633            {
634                Node pluginNode = (Node) plugins.next();
635                getNode().getSession().getWorkspace().copy(pluginNode.getPath(), clonedSite.getPath() + "/ametys-internal:plugins/" + pluginNode.getName());
636            }
637            
638            // Copy sitemaps (ametys-internal:sitemaps is auto-created)
639            NodeIterator sitemaps = getNode().getNode("ametys-internal:sitemaps").getNodes();
640            while (sitemaps.hasNext())
641            {
642                Node sitemapNode = (Node) sitemaps.next();
643                getNode().getSession().getWorkspace().copy(sitemapNode.getPath(), clonedSite.getPath() + "/ametys-internal:sitemaps/" + sitemapNode.getName());
644            }
645            
646            // Copy contents (ametys-internal:contents is auto-created)
647            NodeIterator contents = getNode().getNode("ametys-internal:contents").getNodes();
648            while (contents.hasNext())
649            {
650                Node contentNode = (Node) contents.next();
651                getNode().getSession().getWorkspace().copy(contentNode.getPath(), clonedSite.getPath() + "/ametys-internal:contents/" + contentNode.getName());
652            }
653            
654            if (parent instanceof Site)
655            {
656                return ((Site) parent).getSite(name);
657            }
658            else
659            {
660                return parent.getChild(name);
661            }
662        }
663        catch (RepositoryException e)
664        {
665            throw new AmetysRepositoryException(e);
666        }
667    }
668    
669    @Override
670    public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
671    {
672        return copyTo(parent, name);
673    }
674    
675    private Node _getParentNode (ModifiableTraversableAmetysObject parent, String siteName) throws AmetysRepositoryException
676    {
677        try
678        {
679            if (parent instanceof Site)
680            {
681                return ((Site) parent).getNode().getNode("ametys-internal:sites");
682            }
683            else if (parent instanceof SimpleAmetysObject)
684            {
685                String[] hash = _getHashedPath(siteName);
686                
687                Node contextNode = ((SimpleAmetysObject) parent).getNode();
688                
689                for (String hashPart : hash)
690                {
691                    if (!contextNode.hasNode(hashPart))
692                    {
693                        contextNode = contextNode.addNode(hashPart, AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE);
694                    }
695                    else
696                    {
697                        contextNode = contextNode.getNode(hashPart); 
698                    }
699                }
700                
701                return contextNode;
702            }
703        }
704        catch (RepositoryException e)
705        {
706            throw new AmetysRepositoryException("Unable to create child: " + siteName, e);
707        }
708        
709        throw new AmetysRepositoryException("Unable to get JCR node of object : " + parent.getId());
710    }
711    
712    private String[] _getHashedPath(String name)
713    {
714        long hash = Math.abs(HashUtil.hash(name));
715        String hashStr = Long.toString(hash, 16);
716        hashStr = StringUtils.leftPad(hashStr, 4, '0');
717        
718        return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)};
719    }
720    
721    @Override
722    public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException
723    {
724        throw new UnsupportedOperationException("Site ordering is not supported");
725    }
726}