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