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