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