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