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