- add moved class to core-sitemanagement ; extract classes related to metadatas

git-svn-id: https://svn.apache.org/repos/asf/lenya/trunk@1034333 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/metadata/usecases/Metadata.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/metadata/usecases/Metadata.java
deleted file mode 100644
index fddbff6..0000000
--- a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/metadata/usecases/Metadata.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- * 
- */
-package org.apache.lenya.cms.metadata.usecases;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.lenya.cms.metadata.Element;
-import org.apache.lenya.cms.metadata.MetaData;
-import org.apache.lenya.cms.metadata.MetaDataRegistry;
-import org.apache.lenya.cms.publication.Document;
-import org.apache.lenya.cms.publication.Node;
-import org.apache.lenya.cms.site.usecases.SiteUsecase;
-import org.apache.lenya.cms.usecase.UsecaseException;
-import org.apache.lenya.cms.workflow.WorkflowUtil;
-
-/**
- * Usecase to edit metadata for a resource.
- * 
- * @version $Id$
- */
-public class Metadata extends SiteUsecase {
-    
-    private MetaDataRegistry metaDataRegistry;
-
-    /**
-     * @see org.apache.lenya.cms.usecase.AbstractUsecase#getNodesToLock()
-     */
-    protected Node[] getNodesToLock() throws UsecaseException {
-        Node[] objects = new Node[0];
-        if(getSourceDocument() != null) {
-            objects = new Node[] { getSourceDocument() };
-        }
-        return objects;
-    }
-    
-    /**
-     * Object to pass a meta data entry to the view.
-     */
-    public static class MetaDataWrapper {
-        
-        private String[] values;
-        private Element element;
-        private boolean editable;
-        
-        /**
-         * @param element The element.
-         * @param values The values for the element.
-         * @param canChange If the element value can be changed via the GUI. A <code>true</code>
-         *     value is only effective if the element itself is editable.
-         */
-        public MetaDataWrapper(Element element, String[] values, boolean canChange) {
-            this.values = values;
-            this.element = element;
-            this.editable = element.isEditable() && canChange;
-        }
-        
-        /**
-         * @return The values for the element.
-         */
-        public String[] getValues() {
-            return this.values;
-        }
-        
-        /**
-         * @return The element.
-         */
-        public Element getElement() {
-            return this.element;
-        }
-        
-        /**
-         * @return If the value can be changed via the GUI.
-         */
-        public boolean isEditable() {
-            return this.editable;
-        }
-        
-    }
-
-    /**
-     * @see org.apache.lenya.cms.usecase.AbstractUsecase#initParameters()
-     */
-    protected void initParameters() {
-        super.initParameters();
-        
-        Document doc = getSourceDocument();
-        if (doc == null) {
-            return;
-        }
-        
-
-        try {
-            boolean canChange = WorkflowUtil.canInvoke(doc, "edit");
-            
-            if (!canChange) {
-                addInfoMessage("cannot-change-metadata");
-            }
-            
-            List numbers = new ArrayList();
-            Map num2namespace = new HashMap();
-            List keyList = new ArrayList();
-
-            String[] namespaces = getMetaDataRegistry().getNamespaceUris();
-
-            for (int nsIndex = 0; nsIndex < namespaces.length; nsIndex++) {
-                MetaData meta = doc.getMetaData(namespaces[nsIndex]);
-                String[] keys = meta.getPossibleKeys();
-                for (int keyIndex = 0; keyIndex < keys.length; keyIndex++) {
-                    String key = "ns" + nsIndex + "." + keys[keyIndex];
-                    String[] values = meta.getValues(keys[keyIndex]);
-                    Element element = meta.getElementSet().getElement(keys[keyIndex]);
-                    setParameter(key, new MetaDataWrapper(element, values, canChange));
-                    keyList.add(key);
-                }
-                numbers.add("" + nsIndex);
-                num2namespace.put("" + nsIndex, namespaces[nsIndex]);
-            }
-
-            setParameter("numbers", numbers);
-            setParameter("namespaces", num2namespace);
-
-            Collections.sort(keyList);
-            setParameter("keys", keyList);
-
-        } catch (Exception e) {
-            getLogger().error("Unable to load meta data.", e);
-            addErrorMessage("Unable to load meta data: " + e.getMessage());
-        }
-    }
-
-    protected MetaDataRegistry getMetaDataRegistry() {
-        return metaDataRegistry;
-    }
-
-    public void setMetaDataRegistry(MetaDataRegistry metaDataRegistry) {
-        this.metaDataRegistry = metaDataRegistry;
-    }
-    
-}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractLink.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractLink.java
new file mode 100644
index 0000000..fe01f12
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractLink.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+/* @version $Id$ */
+
+package org.apache.lenya.cms.site;
+
+import org.apache.lenya.cms.publication.Document;
+import org.apache.lenya.cms.publication.Publication;
+
+/**
+ * The AbstractLink class encapsulates a string label and a associated language.
+ */
+public abstract class AbstractLink implements Link {
+    private String label = null;
+    private String language = null;
+
+    /**
+     * Creates a new AbstractLink object.
+     * @param node The site node.
+     * @param _label the actual label
+     * @param _language the language
+     */
+    public AbstractLink(SiteNode node, String _label, String _language) {
+        this.label = _label;
+        this.language = _language;
+        this.node = node;
+    }
+
+    /**
+     * Get the actual label of the AbstractLink object
+     * 
+     * @return the actual label as a String
+     */
+    public String getLabel() {
+        return this.label;
+    }
+
+    /**
+     * Get the language of this AbstractLink object
+     * 
+     * @return the language
+     */
+
+    public String getLanguage() {
+        return this.language;
+    }
+
+    /**
+     * (non-Javadoc)
+     * @see java.lang.Object#toString()
+     */
+    public String toString() {
+        return getLabel() + " " + getLanguage();
+    }
+
+    /**
+     * (non-Javadoc)
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    public boolean equals(Object obj) {
+        boolean equals = false;
+
+        if (getClass().isInstance(obj)) {
+            AbstractLink otherLabel = (AbstractLink) obj;
+            equals = getLabel().equals(otherLabel.getLabel())
+                    && getLanguage().equals(otherLabel.getLanguage());
+        }
+
+        return equals;
+    }
+
+    /**
+     * (non-Javadoc)
+     * @see java.lang.Object#hashCode()
+     */
+    public int hashCode() {
+        return getLabel().hashCode() + getLanguage().hashCode();
+    }
+
+    private SiteNode node;
+
+    public Document getDocument() {
+        SiteNode node = getNode();
+        String uuid = node.getUuid();
+        if (uuid == null) {
+            throw new UnsupportedOperationException("The node [" + node + "] has no UUID.");
+        }
+        Publication pub = node.getStructure().getPublication();
+        String area = node.getStructure().getArea();
+        return pub.getArea(area).getDocument(uuid, getLanguage());
+    }
+
+    public SiteNode getNode() {
+        return this.node;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+        save();
+    }
+
+    protected void save() {
+    }
+
+}
\ No newline at end of file
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractSiteManager.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractSiteManager.java
new file mode 100644
index 0000000..0f2f784
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractSiteManager.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package org.apache.lenya.cms.site;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+import org.apache.cocoon.util.AbstractLogEnabled;
+
+/**
+ * Abstract base class for site managers.
+ * 
+ * @version $Id$
+ */
+public abstract class AbstractSiteManager extends AbstractLogEnabled implements SiteManager {
+
+    /**
+     * @see org.apache.lenya.cms.site.SiteManager#sortAscending(org.apache.lenya.cms.publication.util.DocumentSet)
+     */
+    public SiteNode[] sortAscending(SiteNode[] nodes) throws SiteException {
+        if (nodes.length > 0) {
+
+            if (!check(new NodeSet(nodes))) {
+                throw new SiteException("The dependence relation is not a strict partial order!");
+            }
+
+            SiteNode[] sortedNodes = (SiteNode[]) Arrays.asList(nodes).toArray(
+                    new SiteNode[nodes.length]);
+            Arrays.sort(sortedNodes, new NodeComparator());
+            return sortedNodes;
+        } else {
+            return nodes;
+        }
+    }
+
+    /**
+     * Checks if the dependence relation is a strict partial order.
+     * 
+     * @param set The document set to check.
+     * @return A boolean value.
+     * @throws SiteException when something went wrong.
+     */
+    protected boolean check(NodeSet set) throws SiteException {
+        boolean isStrictPartialOrder = isIrreflexive(set) && isAntisymmetric(set)
+                && isTransitive(set);
+        return isStrictPartialOrder;
+    }
+
+    /**
+     * Checks if the dependence relation is antisymmetric.
+     * 
+     * @param set The document set to check.
+     * @return A boolean value.
+     * @throws SiteException when something went wrong.
+     */
+    protected boolean isAntisymmetric(NodeSet set) throws SiteException {
+        SiteNode[] resources = set.getNodes();
+        boolean isAntisymmetric = true;
+        for (int i = 0; i < resources.length; i++) {
+            for (int j = i + 1; j < resources.length; j++) {
+                if (requires(resources[i], resources[j]) && requires(resources[j], resources[i])
+                        && !(resources[i] == resources[j])) {
+                    isAntisymmetric = false;
+                }
+            }
+        }
+        return isAntisymmetric;
+    }
+
+    /**
+     * Checks if the dependence relation is transitive.
+     * 
+     * @param set The document set to check.
+     * @return A boolean value.
+     * @throws SiteException when something went wrong.
+     */
+    protected boolean isTransitive(NodeSet set) throws SiteException {
+        SiteNode[] resources = set.getNodes();
+        boolean isTransitive = true;
+        for (int i = 0; i < resources.length; i++) {
+            for (int j = i + 1; j < resources.length; j++) {
+                for (int k = j + 1; k < resources.length; k++) {
+                    if (requires(resources[i], resources[j])
+                            && requires(resources[j], resources[k])
+                            && !requires(resources[i], resources[k])) {
+                        isTransitive = false;
+                    }
+                }
+            }
+        }
+        return isTransitive;
+    }
+
+    /**
+     * Checks if the dependence relation is irreflexive.
+     * 
+     * @param set The document set.
+     * @return A boolean value
+     * @throws SiteException
+     */
+    protected boolean isIrreflexive(NodeSet set) throws SiteException {
+        SiteNode[] resources = set.getNodes();
+        boolean isIrreflexive = true;
+        for (int i = 0; i < resources.length; i++) {
+            if (requires(resources[i], resources[i])) {
+                isIrreflexive = false;
+            }
+        }
+        return isIrreflexive;
+    }
+
+    /**
+     * Compares nodes according to the dependence relation.
+     */
+    public class NodeComparator implements Comparator {
+
+        /**
+         * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
+         */
+        public int compare(Object arg0, Object arg1) {
+            int result = 0;
+            if (arg0 instanceof SiteNode && arg1 instanceof SiteNode) {
+                SiteNode doc1 = (SiteNode) arg0;
+                SiteNode doc2 = (SiteNode) arg1;
+
+                try {
+                    if (AbstractSiteManager.this.requires(doc1, doc2)) {
+                        result = 1;
+                    } else if (AbstractSiteManager.this.requires(doc2, doc1)) {
+                        result = -1;
+                    }
+                } catch (SiteException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+            return result;
+        }
+    }
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractSiteNode.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractSiteNode.java
new file mode 100644
index 0000000..bcd161f
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/AbstractSiteNode.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+import java.util.Arrays;
+
+import org.apache.avalon.framework.container.ContainerUtil;
+import org.apache.cocoon.util.AbstractLogEnabled;
+import org.apache.commons.lang.Validate;
+import org.apache.commons.logging.Log;
+import org.apache.lenya.cms.publication.Publication;
+
+/**
+ * Abstract site node implementation.
+ */
+public abstract class AbstractSiteNode extends AbstractLogEnabled implements SiteNode {
+
+    private String path;
+    private SiteStructure structure;
+    private String uuid;
+
+    protected AbstractSiteNode(Publication publication, SiteStructure structure, String path,
+            String uuid, Log logger)
+    {
+    	Validate.notNull(structure);
+    	Validate.notNull(path);
+        Validate.isTrue(path.startsWith("/"), "Path must start with /");
+        Validate.notNull(uuid);
+        this.structure = structure;
+        this.path = path;
+        this.uuid = uuid;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public SiteStructure getStructure() {
+        return this.structure;
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj instanceof SiteNode)) {
+            return false;
+        }
+        String thisKey = getKey(getStructure().getPublication(),
+                getStructure().getArea(),
+                getPath());
+        SiteNode node = (SiteNode) obj;
+        String nodeKey = getKey(node.getStructure().getPublication(),
+                node.getStructure().getArea(),
+                node.getPath());
+        return thisKey.equals(nodeKey);
+    }
+
+    public int hashCode() {
+        return getKey(getStructure().getPublication(), getStructure().getArea(), getPath()).hashCode();
+    }
+
+    protected static String getKey(Publication pub, String area, String docId) {
+        return pub.getId() + ":" + area + ":" + docId;
+    }
+
+    public SiteNode getParent() throws SiteException {
+        String id = getPath().substring(1);
+        String[] steps = id.split("/");
+        if (steps.length == 1) {
+            throw new SiteException("The node [" + getPath() + "] is a top-level node.");
+        } else {
+            int lastIndex = id.lastIndexOf("/");
+            String parentId = id.substring(0, lastIndex);
+            return getStructure().getNode("/" + parentId);
+        }
+    }
+    
+    public boolean isTopLevel() {
+        return getPath().lastIndexOf("/") == 0;
+    }
+
+    public String getUuid() {
+        return this.uuid;
+    }
+
+    public boolean hasLink(String language) {
+        return Arrays.asList(getLanguages()).contains(language);
+    }
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/Link.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/Link.java
new file mode 100644
index 0000000..d7e3f14
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/Link.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+import org.apache.lenya.cms.publication.Document;
+
+/**
+ * A link in the site structure references a document.
+ * A site node contains a link for each translation.
+ */
+public interface Link {
+
+    /**
+     * @return The language of this link.
+     */
+    String getLanguage();
+    
+    /**
+     * @return The document this link points to.
+     */
+    Document getDocument();
+
+    /**
+     * @return The node this link belongs to.
+     */
+    SiteNode getNode();
+
+    /**
+     * @return The label of this link.
+     */
+    String getLabel();
+    
+    /**
+     * @param label The new label.
+     */
+    void setLabel(String label);
+    
+    /**
+     * Removes the link.
+     */
+    void delete();
+    
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/NodeIterator.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/NodeIterator.java
new file mode 100644
index 0000000..9806b82
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/NodeIterator.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.commons.lang.Validate;
+
+/**
+ * Node iterator.
+ */
+public class NodeIterator {
+
+    private Iterator delegate;
+    
+    /**
+     * @param collection The collection to iterate over.
+     */
+    public NodeIterator(Collection collection) {
+        Validate.notNull(collection);
+        this.delegate = collection.iterator();
+    }
+    
+    /**
+     * @param nodes The nodes to iterate over.
+     */
+    public NodeIterator(SiteNode[] nodes) {
+    	Validate.notNull(nodes);
+        this.delegate = Arrays.asList(nodes).iterator();
+    }
+    
+    /**
+     * @return A site node.
+     * @see Iterator#next()
+     */
+    public SiteNode next() {
+        return (SiteNode) this.delegate.next();
+    }
+    
+    /**
+     * @return A boolean value.
+     * @see Iterator#hasNext()
+     */
+    public boolean hasNext() {
+        return this.delegate.hasNext();
+    }
+    
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/NodeSet.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/NodeSet.java
new file mode 100644
index 0000000..c0ee4cd
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/NodeSet.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.cocoon.spring.configurator.WebAppContextUtils;
+import org.apache.lenya.cms.publication.Document;
+import org.apache.lenya.cms.publication.DocumentException;
+import org.apache.lenya.cms.publication.util.DocumentSet;
+
+/**
+ * A set containing nodes.
+ */
+public class NodeSet {
+
+    /**
+     * Ctor.
+     * @param manager The service manager.
+     */
+    public NodeSet() {
+    }
+
+    /**
+     * Ctor.
+     * @param _nodes The initial nodes.
+     */
+    public NodeSet(SiteNode[] _nodes) {
+        for (int i = 0; i < _nodes.length; i++) {
+            add(_nodes[i]);
+        }
+    }
+
+    /**
+     * Ctor.
+     * @param documents The corresponding documents to derive nodes from.
+     */
+    public NodeSet(DocumentSet documents) {
+        Document[] docs = documents.getDocuments();
+        for (int i = 0; i < docs.length; i++) {
+            SiteNode node;
+            try {
+                node = docs[i].getLink().getNode();
+            } catch (DocumentException e) {
+                throw new RuntimeException(e);
+            }
+            if (!contains(node)) {
+                add(node);
+            }
+        }
+    }
+
+    /**
+     * @param node A node.
+     * @return If the node is contained.
+     */
+    public boolean contains(SiteNode node) {
+        return getSet().contains(node);
+    }
+
+    private Set nodes = new HashSet();
+
+    /**
+     * Returns the list object that stores the documents.
+     * @return A list.
+     */
+    protected Set getSet() {
+        return this.nodes;
+    }
+
+    /**
+     * Returns the documents contained in this set.
+     * 
+     * @return An array of documents.
+     */
+    public SiteNode[] getNodes() {
+        return (SiteNode[]) this.nodes.toArray(new SiteNode[this.nodes.size()]);
+    }
+
+    /**
+     * Adds a node to this set.
+     * @param node The node to add.
+     */
+    public void add(SiteNode node) {
+        assert node != null;
+        assert !this.nodes.contains(node);
+        this.nodes.add(node);
+    }
+
+    /**
+     * Checks if this set is empty.
+     * @return A boolean value.
+     */
+    public boolean isEmpty() {
+        return getSet().isEmpty();
+    }
+
+    /**
+     * Removes a node.
+     * @param resource The node.
+     */
+    public void remove(SiteNode resource) {
+        assert resource != null;
+        assert getSet().contains(resource);
+        getSet().remove(resource);
+    }
+
+    /**
+     * Removes all nodes.
+     */
+    public void clear() {
+        getSet().clear();
+    }
+
+    /**
+     * @return An iterator iterating in undetermined order.
+     */
+    public NodeIterator iterator() {
+        return new NodeIterator(getNodes());
+    }
+
+    /**
+     * @return An iterator iterating in ascending order.
+     */
+    public NodeIterator ascending() {
+        SiteNode[] nodes = getNodesAscending();
+        return new NodeIterator(nodes);
+    }
+
+    /**
+     * @return An iterator iterating in descending order.
+     */
+    public NodeIterator descending() {
+        SiteNode[] nodes = getNodesAscending();
+        List list = Arrays.asList(nodes);
+        Collections.reverse(list);
+        return new NodeIterator(list);
+    }
+
+    protected SiteNode[] getNodesAscending() {
+        if (isEmpty()) {
+            return new SiteNode[0];
+        }
+
+        try {
+            String hint = getNodes()[0].getStructure().getPublication().getSiteManagerHint();
+            SiteManager siteManager = (SiteManager) WebAppContextUtils.getCurrentWebApplicationContext().getBean(
+                    SiteManager.class.getName() + "/" + hint);
+            return siteManager.sortAscending(getNodes());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * @return All documents referenced by this node set.
+     */
+    public Document[] getDocuments() {
+        List documents = new ArrayList();
+        for (NodeIterator i = iterator(); i.hasNext();) {
+            SiteNode node = i.next();
+            String[] langs = node.getLanguages();
+            for (int l = 0; l < langs.length; l++) {
+                try {
+                    documents.add(node.getLink(langs[l]).getDocument());
+                } catch (SiteException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+        return (Document[]) documents.toArray(new Document[documents.size()]);
+    }
+
+    /**
+     * Adds all nodes from a node set to this.
+     * @param set The set.
+     */
+    public void addAll(NodeSet set) {
+        this.nodes.addAll(set.getSet());
+    }
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteException.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteException.java
new file mode 100644
index 0000000..a0acfc3
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteException.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.lenya.cms.site;
+
+import org.apache.lenya.cms.publication.PublicationException;
+
+/**
+ * Site structure management exception.
+ * 
+ * @version $Id$
+ */
+public class SiteException extends PublicationException {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+     * Ctor.
+     */
+    public SiteException() {
+        super();
+    }
+
+    /**
+     * Ctor.
+     * @param message The message.
+     */
+    public SiteException(String message) {
+        super(message);
+    }
+
+    /**
+     * Ctor.
+     * @param cause The cause.
+     */
+    public SiteException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Ctor.
+     * @param message The message.
+     * @param cause The cause.
+     */
+    public SiteException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteManager.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteManager.java
new file mode 100644
index 0000000..a1c83e3
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteManager.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package org.apache.lenya.cms.site;
+
+import org.apache.lenya.cms.publication.Document;
+import org.apache.lenya.cms.publication.DocumentLocator;
+import org.apache.lenya.cms.publication.Publication;
+import org.apache.lenya.cms.publication.Session;
+
+/**
+ * <p>
+ * A site structure management component.
+ * </p>
+ * 
+ * <p>
+ * A site manager has a dependence relation, which is always applied to documents of a single
+ * language. This means a document may not require a document of another language. Dependence on a
+ * set of resources must be a strict partial order <strong>&lt; </strong>:
+ * </p>
+ * <ul>
+ * <li><em>irreflexive:</em> d <strong>&lt; </strong>d does not hold for any resource d</li>
+ * <li><em>antisymmetric:</em> d <strong>&lt; </strong>e and e <strong>&lt; </strong>d implies d=e</li>
+ * <li><em>transitive:</em> d <strong>&lt; </strong>e and e <strong>&lt; </strong>f implies d
+ * <strong>&lt; </strong>f</li>
+ * </ul>
+ * 
+ * @version $Id$
+ */
+public interface SiteManager {
+
+    /**
+     * The Avalon role.
+     */
+    String ROLE = SiteManager.class.getName();
+
+    /**
+     * Checks if a resource requires another one.
+     * @param dependingResource The depending resource.
+     * @param requiredResource The required resource.
+     * @return A boolean value.
+     * @throws SiteException if an error occurs.
+     */
+    boolean requires(SiteNode dependingResource, SiteNode requiredResource) throws SiteException;
+
+    /**
+     * Returns the resources which are required by a certain resource.
+     * 
+     * @param session The session to operate on.
+     * @param locator The depending locator.
+     * @return An array of resources.
+     * @throws SiteException if an error occurs.
+     */
+    DocumentLocator[] getRequiredResources(Session session, DocumentLocator locator)
+            throws SiteException;
+
+    /**
+     * Returns the resources which require a certain resource.
+     * 
+     * @param resource The required resource.
+     * @return An array of resources.
+     * @throws SiteException if an error occurs.
+     */
+    SiteNode[] getRequiringResources(SiteNode resource) throws SiteException;
+
+    /**
+     * Adds a document to the site structure.
+     * @param path The path.
+     * @param document The document to add.
+     * @throws SiteException if the document is already contained.
+     */
+    void add(String path, Document document) throws SiteException;
+
+    /**
+     * Sets a document to the site structure.
+     * @param path The path.
+     * @param document The document to add.
+     * @throws SiteException if the document is already contained or if the path doesn't exist.
+     */
+    void set(String path, Document document) throws SiteException;
+
+    /**
+     * Checks if the site structure contains a certain resource in a certain area.
+     * 
+     * @param resource The resource.
+     * @return A boolean value.
+     * @throws SiteException if an error occurs.
+     */
+    boolean contains(Document resource) throws SiteException;
+
+    /**
+     * Checks if the site structure contains any language version of a certain resource in a certain
+     * area.
+     * 
+     * @param resource The resource.
+     * @return A boolean value.
+     * @throws SiteException if an error occurs.
+     */
+    boolean containsInAnyLanguage(Document resource) throws SiteException;
+
+    /**
+     * Copies a document in the site structure.
+     * 
+     * @param sourceDocument The source document.
+     * @param destinationDocument The destination document.
+     * @throws SiteException when something went wrong.
+     */
+    void copy(Document sourceDocument, Document destinationDocument) throws SiteException;
+
+    /**
+     * Sets the visibility of a node in the navigation. It is meant to hide specific nodes within
+     * the "public" navigation whereas the node is visible within the info/site area.
+     * 
+     * @param document The document.
+     * @param visibleInNav The visibility.
+     * @throws SiteException if an error occurs.
+     */
+    void setVisibleInNav(Document document, boolean visibleInNav) throws SiteException;
+
+    /**
+     * Returns the visibility of a node in the navigation.
+     * 
+     * @param document The document.
+     * @return A boolean value.
+     * @throws SiteException if an error occurs.
+     */
+    boolean isVisibleInNav(Document document) throws SiteException;
+
+    /**
+     * Returns all documents in a certain area.
+     * 
+     * @param publication The publication.
+     * @param area The area.
+     * @return An array of documents.
+     * @throws SiteException if an error occurs.
+     */
+    Document[] getDocuments(Publication publication, String area) throws SiteException;
+
+    /**
+     * Sorts a set of nodes using the "requires" relation.
+     * 
+     * @param nodes The set.
+     * @return A sorted array of nodes.
+     * @throws SiteException if an error occurs.
+     */
+    SiteNode[] sortAscending(SiteNode[] nodes) throws SiteException;
+
+    /**
+     * @param publication The publication.
+     * @param area The area.
+     * @return The object that holds the site structure information.
+     * @throws SiteException if an error occurs.
+     */
+    SiteStructure getSiteStructure(Publication publication, String area) throws SiteException;
+
+    /**
+     * Checks if the document does already exist. If it does, returns a non-existing document with a
+     * similar document ID. If it does not, the original document is returned.
+     * @param session The session.
+     * @param locator The locator.
+     * @return A locator.
+     * @throws SiteException if the new document could not be built.
+     */
+    DocumentLocator getAvailableLocator(Session session, DocumentLocator locator)
+            throws SiteException;
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteNode.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteNode.java
new file mode 100644
index 0000000..af9936c
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteNode.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+/**
+ * A node in the site structure.
+ */
+public interface SiteNode {
+    
+    /**
+     * @return The site structure this node belongs to.
+     */
+    SiteStructure getStructure();
+
+    /**
+     * @return The path.
+     */
+    String getPath();
+
+    /**
+     * @return The parent node.
+     * @throws SiteException If the node has no parent.
+     */
+    SiteNode getParent() throws SiteException;
+    
+    /**
+     * @return If this is a top level node. Top level nodes have no parents.
+     */
+    boolean isTopLevel();
+    
+    /**
+     * @return The languages of this node.
+     */
+    String[] getLanguages();
+    
+    /**
+     * @param language The language.
+     * @return The link for the language.
+     * @throws SiteException if no link is contained for the language.
+     */
+    Link getLink(String language) throws SiteException;
+
+    /**
+     * @return The UUID of this node.
+     */
+    String getUuid();
+
+    /**
+     * Checks if a link for a certain language is contained.
+     * @param language The language.
+     * @return A boolean value.
+     */
+    boolean hasLink(String language);
+
+    /**
+     * @return The name, i.e. the last path element.
+     */
+    String getName();
+    
+    /**
+     * @return if the node is visible in the navigation.
+     */
+    boolean isVisible();
+
+    /**
+     * Sets the node visibility in the navigation.
+     * @param visibleInNav if the node should be visible.
+     */
+    void setVisible(boolean visibleInNav);
+
+    /**
+     * Deletes this node.
+     */
+    void delete();
+
+    /**
+     * @return The children of this node.
+     */
+    SiteNode[] getChildren();
+
+    /**
+     * @return if the node has an external link.
+     */
+    boolean hasLink();
+
+    /**
+     * @return The external link.
+     */
+    String getHref();
+
+    /**
+     * @return The suffix.
+     */
+    String getSuffix();
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteStructure.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteStructure.java
new file mode 100644
index 0000000..dc7f3cb
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteStructure.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+import org.apache.lenya.cms.publication.Document;
+import org.apache.lenya.cms.publication.Node;
+import org.apache.lenya.cms.publication.Publication;
+
+/**
+ * Object to hold a site structure information.
+ * 
+ * @version $Id$
+ */
+public interface SiteStructure extends Node {
+
+    /**
+     * @return The publication.
+     */
+    Publication getPublication();
+
+    /**
+     * @return The area.
+     */
+    String getArea();
+
+    /**
+     * @return All nodes in this structure.
+     */
+    SiteNode[] getNodes();
+
+    /**
+     * @param path The path.
+     * @return A site node.
+     * @throws SiteException if no node is contained for the path.
+     */
+    SiteNode getNode(String path) throws SiteException;
+
+    /**
+     * Checks if a node is contained for a certain path.
+     * @param path The path.
+     * @return A boolean value.
+     */
+    boolean contains(String path);
+
+    /**
+     * Checks if a link is contained for a certain path and language.
+     * @param path The path.
+     * @param language The language.
+     * @return A boolean value.
+     */
+    boolean contains(String path, String language);
+
+    /**
+     * Checks if the structure contains a link with a certain UUID and language.
+     * @param uuid The UUID.
+     * @param language The language.
+     * @return A boolean value.
+     */
+    boolean containsByUuid(String uuid, String language);
+
+    /**
+     * Checks if the structure contains any language version of a document.
+     * @param uuid The uuid.
+     * @return A boolean value.
+     */
+    boolean containsInAnyLanguage(String uuid);
+
+    /**
+     * Returns a node for a certain UUID.
+     * @param uuid The UUID.
+     * @param language The language.
+     * @return a link.
+     * @throws SiteException if no node is contained for the UUID.
+     */
+    Link getByUuid(String uuid, String language) throws SiteException;
+
+    /**
+     * Adds a link to a document.
+     * @param path The path.
+     * @param doc The document.
+     * @return A link.
+     * @throws SiteException if the document is already contained or the node
+     *         for this path already contains a link for this language.
+     */
+    Link add(String path, Document doc) throws SiteException;
+
+    /**
+     * Adds a site node.
+     * @param path The path.
+     * @return A site node.
+     * @throws SiteException if the path is already contained.
+     */
+    SiteNode add(String path) throws SiteException;
+
+    /**
+     * Adds a site node before a specific other node.
+     * @param path The path.
+     * @param followingSiblingPath The path of the node which will be the
+     *        following sibling of the node to insert.
+     * @return A site node.
+     * @throws SiteException if the path is already contained.
+     */
+    SiteNode add(String path, String followingSiblingPath) throws SiteException;
+    
+    /**
+     * @return The top level nodes.
+     */
+    SiteNode[] getTopLevelNodes();
+
+}
diff --git a/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteUtil.java b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteUtil.java
new file mode 100644
index 0000000..53a27d7
--- /dev/null
+++ b/org.apache.lenya.core.sitemanagement/src/main/java/org/apache/lenya/cms/site/SiteUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.lenya.cms.site;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.cocoon.spring.configurator.WebAppContextUtils;
+import org.apache.lenya.cms.publication.DocumentLocator;
+import org.apache.lenya.cms.publication.Publication;
+import org.apache.lenya.cms.publication.Session;
+
+/**
+ * Utility to handle site structures.
+ * 
+ * @version $Id$
+ */
+public class SiteUtil {
+
+    private SiteUtil() {
+    }
+
+    /**
+     * Returns a sub-site starting with a certain node, which includes the node itself and all nodes
+     * which require this node, in preorder.
+     * 
+     * @param node The top-level document.
+     * @return A document set.
+     * @throws SiteException if an error occurs.
+     */
+    public static NodeSet getSubSite(SiteNode node) throws SiteException {
+        SiteManager siteManager = null;
+        SiteNode[] subsite;
+        try {
+            String hint = node.getStructure().getPublication().getSiteManagerHint();
+            siteManager = (SiteManager) WebAppContextUtils.getCurrentWebApplicationContext()
+                    .getBean(SiteManager.class.getName() + "/" + hint);
+
+            Set nodes = new HashSet();
+            nodes.add(node);
+
+            SiteNode[] requiringNodes = siteManager.getRequiringResources(node);
+            for (int i = 0; i < requiringNodes.length; i++) {
+                nodes.add(requiringNodes[i]);
+            }
+
+            subsite = (SiteNode[]) nodes.toArray(new SiteNode[nodes.size()]);
+        } catch (Exception e) {
+            throw new SiteException(e);
+        }
+        return new NodeSet(subsite);
+    }
+
+    /**
+     * @see org.apache.lenya.cms.site.SiteManager#getAvailableLocator(DocumentFactory,
+     *      DocumentLocator)
+     * @param factory The factory.
+     * @param locator The locator.
+     * @return A document.
+     * @throws SiteException if an error occurs.
+     */
+    public static DocumentLocator getAvailableLocator(Session session,
+            DocumentLocator locator) throws SiteException {
+        SiteManager siteManager = null;
+        try {
+            Publication pub = session.getPublication(locator.getPublicationId());
+            siteManager = (SiteManager) WebAppContextUtils.getCurrentWebApplicationContext()
+                    .getBean(SiteManager.ROLE + "/" + pub.getSiteManagerHint());
+            return siteManager.getAvailableLocator(session, locator);
+        } catch (Exception e) {
+            throw new SiteException(e);
+        }
+    }
+
+}
\ No newline at end of file