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