move all document related classes from publication to document

git-svn-id: https://svn.apache.org/repos/asf/lenya/trunk@1034527 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DefaultDocumentBuilder.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DefaultDocumentBuilder.java
new file mode 100644
index 0000000..bd3ad6e
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DefaultDocumentBuilder.java
@@ -0,0 +1,208 @@
+/*
+ * 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.publication;
+
+import java.net.MalformedURLException;
+
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.avalon.framework.thread.ThreadSafe;
+import org.apache.cocoon.util.AbstractLogEnabled;
+import org.apache.lenya.cms.site.SiteNode;
+
+/**
+ * Default document builder implementation.
+ * 
+ * @version $Id$
+ */
+public class DefaultDocumentBuilder extends AbstractLogEnabled implements DocumentBuilder,
+        Serviceable, ThreadSafe {
+
+    /**
+     * Ctor.
+     */
+    public DefaultDocumentBuilder() {
+    }
+
+    /**
+     * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
+     */
+    public void service(ServiceManager manager) {
+        this.manager = manager;
+    }
+
+    protected ServiceManager manager;
+
+    /**
+     * Removes all "."-separated extensions from a URL (e.g.,
+     * <code>/foo.print.html</code> is transformed to <code>/foo</code>).
+     * @param url The URL to trim.
+     * @return A URL string.
+     */
+    protected String removeExtensions(String url) {
+        int dotIndex = url.indexOf(".");
+        if (dotIndex > -1) {
+            url = url.substring(0, dotIndex);
+        }
+        return url;
+    }
+
+    /**
+     * Returns the language of a URL.
+     * @param urlWithoutSuffix The URL without the suffix.
+     * @return A string.
+     */
+    protected String getLanguage(String urlWithoutSuffix) {
+
+        String language = "";
+        String url = urlWithoutSuffix;
+
+        int languageSeparatorIndex = url.lastIndexOf("_");
+        if (languageSeparatorIndex > -1) {
+            String suffix = url.substring(languageSeparatorIndex + 1);
+            if (suffix.length() <= 5) {
+                language = suffix;
+            }
+        }
+        return language;
+    }
+
+    /**
+     * Returns the extension of a URL.
+     * @param url The URL.
+     * @return The extension.
+     */
+    protected String getExtension(String url) {
+        int startOfSuffix = url.lastIndexOf('.');
+        String suffix = "";
+
+        if ((startOfSuffix > -1) && !url.endsWith(".")) {
+            suffix = url.substring(startOfSuffix + 1);
+        }
+
+        return suffix;
+    }
+
+    public boolean isDocument(Session session, String url) {
+        try {
+            DocumentLocator locator = getLocatorWithoutCheck(session, url);
+            if (locator != null) {
+                Publication pub = session.getPublication(locator.getPublicationId());
+                String path = locator.getPath();
+                Area area = pub.getArea(locator.getArea());
+                if (area.getSite().contains(path)) {
+                    SiteNode node = area.getSite().getNode(path);
+                    if (node.hasLink(locator.getLanguage())) {
+                        return true;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        return false;
+    }
+
+    /**
+     * Builds the canonical document URL.
+     * @param session The document factory.
+     * @param locator The document locator.
+     * @return A string.
+     */
+    protected String buildCanonicalDocumentUrl(Session session, DocumentLocator locator) {
+
+        String languageSuffix = "";
+        String language = locator.getLanguage();
+
+        Publication pub = session.getPublication(locator.getPublicationId());
+
+        if (!language.equals(pub.getDefaultLanguage())) {
+            languageSuffix = "_" + language;
+        }
+
+        return locator.getPath() + languageSuffix + ".html";
+    }
+
+    public String buildCanonicalUrl(Session session, DocumentLocator doc) {
+
+        String documentUrl = buildCanonicalDocumentUrl(session, doc);
+        String url = "/" + doc.getPublicationId() + "/" + doc.getArea() + documentUrl;
+        return url;
+    }
+
+    public DocumentLocator getLocator(Session session, String webappUrl) throws MalformedURLException {
+
+        DocumentLocator locator = getLocatorWithoutCheck(session, webappUrl);
+        if (locator == null) {
+            throw new ResourceNotFoundException("The webapp URL [" + webappUrl
+                    + "] does not refer to a document!");
+        }
+        return locator;
+    }
+
+    /**
+     * Creates a document locator for a webapp URL without checking if the
+     * webapp URL refers to a locator first.
+     * @param session The document factory.
+     * @param webappUrl The webapp URL.
+     * @return A document locator or <code>null</code> if the URL doesn't
+     *         refer to a locator.
+     * @throws MalformedURLException if the URL is not a webapp URL. 
+     */
+    protected DocumentLocator getLocatorWithoutCheck(Session session, String webappUrl) throws MalformedURLException {
+
+        if (!webappUrl.startsWith("/")) {
+            return null;
+        }
+        if (webappUrl.substring(1).split("/").length < 3) {
+            return null;
+        }
+
+        URLInformation info = new URLInformation(webappUrl);
+
+        Publication publication = session.getPublication(info.getPublicationId());
+        String documentURL = info.getDocumentUrl();
+        documentURL = removeExtensions(documentURL);
+
+        String language = getLanguage(documentURL);
+        String fullLanguage = "".equals(language) ? "" : ("_" + language);
+        documentURL = documentURL.substring(0, documentURL.length() - fullLanguage.length());
+
+        if ("".equals(language)) {
+            language = publication.getDefaultLanguage();
+        }
+
+        String path = documentURL;
+
+        if (!path.startsWith("/")) {
+            throw new MalformedURLException("Path [" + path + "] does not start with '/'!");
+        }
+
+        return DocumentLocator.getLocator(publication.getId(), info.getArea(), path, language);
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentBuilder#isValidDocumentName(java.lang.String)
+     */
+    public boolean isValidDocumentName(String documentName) {
+        return documentName.matches("[a-zA-Z0-9\\-]+");
+    }
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DefaultDocumentIdToPathMapper.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DefaultDocumentIdToPathMapper.java
new file mode 100644
index 0000000..1086670
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DefaultDocumentIdToPathMapper.java
@@ -0,0 +1,88 @@
+/*
+ * 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.publication;
+
+import java.io.File;
+
+/**
+ * Default DocumentIdToPathMapper implementation.
+ * 
+ * @version $Id$
+ */
+public class DefaultDocumentIdToPathMapper implements DocumentIdToPathMapper {
+
+    /**
+     * The file name.
+     */
+    public static final String BASE_FILENAME_PREFIX = "index";
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentIdToPathMapper#getPath(java.lang.String,
+     *      java.lang.String)
+     */
+    public String getPath(String uuid, String language) {
+        if (uuid.startsWith("/")) {
+            return uuid.substring(1) + "/" + getFilename(language);
+        }
+        else {
+            return uuid + "/" + language;
+        }
+    }
+
+    /**
+     * Constructs the filename for a given language.
+     * 
+     * @param language The language.
+     * @return A string value.
+     */
+    protected String getFilename(String language) {
+        String languageSuffix = "";
+        if (language != null && !"".equals(language)) {
+            languageSuffix = "_" + language;
+        }
+        return BASE_FILENAME_PREFIX + languageSuffix;
+    }
+
+    /**
+     * Returns the language for a certain file
+     * 
+     * @param file the document file
+     * 
+     * @return the language for the given document file or null if the file has no language.
+     */
+    public String getLanguage(File file) {
+        String fileName = file.getName();
+        String language = null;
+
+        int lastDotIndex = fileName.lastIndexOf(".");
+        String suffix = fileName.substring(lastDotIndex);
+
+        // check if the file is of the form index.html or index_en.html
+
+        if (fileName.startsWith(BASE_FILENAME_PREFIX) && fileName.endsWith(suffix)) {
+            String languageSuffix = fileName.substring(BASE_FILENAME_PREFIX.length(),
+                    fileName.indexOf(suffix));
+            if (languageSuffix.length() > 0) {
+                // trim the leading '_'
+                language = languageSuffix.substring(1);
+            }
+        }
+        return language;
+    }
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/Document.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/Document.java
new file mode 100644
index 0000000..0498b21
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/Document.java
@@ -0,0 +1,307 @@
+/*
+ * 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.publication;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+
+import org.apache.lenya.cms.metadata.MetaDataOwner;
+import org.apache.lenya.cms.publication.util.DocumentVisitor;
+import org.apache.lenya.cms.site.Link;
+
+/**
+ * A CMS document.
+ * @version $Id$
+ */
+public interface Document extends Node, MetaDataOwner {
+    
+    /**
+     * The document namespace URI.
+     */
+    String NAMESPACE = "http://apache.org/cocoon/lenya/document/1.0";
+    
+    /**
+     * The default namespace prefix.
+     */
+    String DEFAULT_PREFIX = "lenya";
+    
+    /**
+     * The transactionable type for document objects.
+     */
+    String TRANSACTIONABLE_TYPE = "document";
+    
+    /**
+     * <code>DOCUMENT_META_SUFFIX</code> The suffix for document meta Uris
+     */
+    final String DOCUMENT_META_SUFFIX = ".meta";
+    
+    /**
+     * Returns the date at which point the requested document is considered expired
+     * @return a string in RFC 1123 date format
+     * @throws DocumentException if an error occurs.
+     */
+    Date getExpires() throws DocumentException;
+
+    /**
+     * Returns the document name of this document.
+     * @return the document-name of this document.
+     */
+    String getName();
+    
+    /**
+     * Returns the publication this document belongs to.
+     * @return A publication object.
+     */
+    Publication getPublication();
+    
+    /**
+     * Returns the canonical web application URL.
+     * @return A string.
+     */
+    String getCanonicalWebappURL();
+
+    /**
+     * Returns the canonical document URL.
+     * @return A string.
+     */
+    String getCanonicalDocumentURL();
+
+    /**
+     * Returns the language of this document.
+     * Each document has one language associated to it. 
+     * @return A string denoting the language.
+     */
+    String getLanguage();
+
+    /**
+     * Returns all the languages this document is available in.
+     * A document has one associated language (@see Document#getLanguage)
+     * but there are possibly a number of other languages for which a 
+     * document with the same document-uuid is also available in. 
+     * 
+     * @return An array of strings denoting the languages.
+     */
+    String[] getLanguages();
+
+    /**
+     * Returns the date of the last modification of this document.
+     * @return A date denoting the date of the last modification.
+     * @throws DocumentException if an error occurs.
+     */
+    long getLastModified() throws DocumentException;
+
+    /**
+     * Returns the area this document belongs to.
+     * @return The area.
+     */
+    String getArea();
+
+    /**
+     * Returns the extension in the URL without the dot.
+     * @return A string.
+     */
+    String getExtension();
+
+    /**
+     * Returns the UUID.
+     * @return A string.
+     */
+    String getUUID();
+    
+    /**
+     * Check if a document with the given document-uuid, language and in the given
+     * area actually exists.
+     * 
+     * @return true if the document exists, false otherwise
+     */
+    boolean exists();
+    
+    /**
+     * Check if a document exists with the given document-uuid and the given area
+     * independently of the given language.
+     * 
+     * @return true if a document with the given document-uuid and area exists, false otherwise
+     */
+    boolean existsInAnyLanguage();
+    
+    /**
+     * Returns the URI to resolve the document's source.
+     * The source can only be used for read-only access.
+     * For write access, use {@link #getOutputStream()}.
+     * @return A string.
+     */
+    String getSourceURI();
+    
+    /**
+     * @return The output stream to write the document content to.
+     */
+    OutputStream getOutputStream();
+    
+    /**
+     * Accepts a document visitor.
+     * @param visitor The visitor.
+     * @throws Exception if an error occurs.
+     */
+    void accept(DocumentVisitor visitor) throws Exception;
+
+    /**
+     * Deletes the document.
+     * @throws DocumentException if an error occurs.
+     */
+    void delete() throws DocumentException;
+    
+    /**
+     * @return The resource type of this document (formerly known as doctype)
+     * @throws DocumentException if the resource type has not been set.
+     */
+    ResourceType getResourceType() throws DocumentException;
+    
+    /**
+     * @param resourceType The resource type of this document.
+     */
+    void setResourceType(ResourceType resourceType);
+    
+    /**
+     * @return The source extension used by this document, without the dot.
+     */
+    String getSourceExtension();
+    
+    /**
+     * @param extension The source extension used by this document, without the dot.
+     */
+    void setSourceExtension(String extension);
+    
+    /**
+     * Sets the mime type of this document.
+     * @param mimeType The mime type.
+     */
+    void setMimeType(String mimeType);
+    
+    /**
+     * @return The mime type of this document.
+     * @throws DocumentException if the mime type has not been set.
+     */
+    String getMimeType() throws DocumentException;
+    
+    /**
+     * @return The content length of the document.
+     */
+    long getContentLength();
+    
+    /**
+     * @return The document identifier for this document.
+     */
+    DocumentIdentifier getIdentifier();
+    
+    /**
+     * This is a shortcut to getLink().getNode().getPath().
+     * @return The path of this document in the site structure.
+     * @throws DocumentException if the document is not linked in the site structure.
+     */
+    String getPath() throws DocumentException;
+
+    /**
+     * Checks if a certain translation (language version) of this document exists.
+     * @param language The language.
+     * @return A boolean value.
+     */
+    boolean existsTranslation(String language);
+    
+    /**
+     * Returns a certain translation (language version) of this document.
+     * @param language The language.
+     * @return A document.
+     * @throws ResourceNotFoundException if the language version doesn't exist.
+     */
+    Document getTranslation(String language) throws ResourceNotFoundException;
+    
+    /**
+     * Checks if this document exists in a certain area.
+     * @param area The area.
+     * @return A boolean value.
+     */
+    boolean existsAreaVersion(String area);
+    
+    /**
+     * Returns the document in a certain area.
+     * @param area The area.
+     * @return A document.
+     * @throws ResourceNotFoundException if the area version doesn't exist.
+     */
+    Document getAreaVersion(String area) throws ResourceNotFoundException;
+
+    /**
+     * Checks if a translation of this document exists in a certain area.
+     * @param area The area.
+     * @param language The language.
+     * @return A boolean value.
+     */
+    boolean existsVersion(String area, String language);
+    
+    /**
+     * Returns a translation of this document in a certain area.
+     * @param area The area.
+     * @param language The language.
+     * @return A document.
+     * @throws ResourceNotFoundException if the area version doesn't exist.
+     */
+    Document getVersion(String area, String language) throws ResourceNotFoundException;
+    
+    /**
+     * @return A document locator.
+     */
+    DocumentLocator getLocator();
+    
+    /**
+     * @return The link to this document in the site structure.
+     * @throws DocumentException if the document is not referenced in the site structure.
+     */
+    Link getLink() throws DocumentException;
+    
+    /**
+     * @return The area the document belongs to.
+     */
+    Area area();
+
+    /**
+     * @return if the document is linked in the site structure.
+     */
+    boolean hasLink();
+
+    /**
+     * @return The input stream to obtain the document's content.
+     */
+    InputStream getInputStream();
+    
+    /**
+     * @param i The revision number.
+     * @return A revision.
+     * @throws RepositoryException if the revision doesn't exist.
+     */
+    Document getRevision(int i) throws RepositoryException;
+
+    /**
+     * @return The revision number of this document.
+     */
+    int getRevisionNumber();
+    
+    History getHistory();
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentBuildException.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentBuildException.java
new file mode 100644
index 0000000..59ebdb8
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentBuildException.java
@@ -0,0 +1,63 @@
+/*
+ * 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.publication;
+
+/**
+ * Document build exception.
+ *
+ * @version $Id$
+ */
+public class DocumentBuildException extends PublicationException {
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+     * Constructor.
+     */
+    public DocumentBuildException() {
+        super();
+    }
+
+    /**
+     * Constructor.
+     * @param message A message.
+     */
+    public DocumentBuildException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor.
+     * @param cause The cause of the exception.
+     */
+    public DocumentBuildException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructor.
+     * @param message A message.
+     * @param cause The cause of the exception.
+     */
+    public DocumentBuildException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentBuilder.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentBuilder.java
new file mode 100644
index 0000000..7a99efa
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentBuilder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ *
+ */
+
+/* $Id$  */
+
+package org.apache.lenya.cms.publication;
+
+import java.net.MalformedURLException;
+
+/**
+ * A document builder builds a document from a URL.
+ */
+public interface DocumentBuilder {
+
+    /**
+     * The Avalon role.
+     */
+    String ROLE = DocumentBuilder.class.getName();
+    
+    /**
+     * Returns a document for a web application URL.
+     * @param factory The factory.
+     * @param webappUrl The web application URL.
+     * @return A document identifier.
+     * @throws MalformedURLException if the URL is not a webapp URL. 
+     */
+    DocumentLocator getLocator(Session session, String webappUrl) throws MalformedURLException;
+
+    /**
+     * Checks if an URL corresponds to a CMS document.
+     * @param factory The document factory.
+     * @param url The URL of the form /{publication-id}/...
+     * @return A boolean value.
+     * @throws DocumentBuildException when something went wrong.
+     */
+    boolean isDocument(Session session, String url);
+
+    /**
+     * Builds an URL corresponding to a CMS document.
+     * @param factory The document factory.
+     * @param locator The locator.
+     * @return a String The corresponding URL.
+     */
+    String buildCanonicalUrl(Session session, DocumentLocator locator);
+
+    /**
+     * Checks if a document name is valid.
+     * @param documentName The document name.
+     * @return A boolean value.
+     */
+    boolean isValidDocumentName(String documentName);
+
+}
\ No newline at end of file
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentDoesNotExistException.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentDoesNotExistException.java
new file mode 100644
index 0000000..7f4f15c
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentDoesNotExistException.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.
+ *
+ */
+
+/* $Id$  */
+
+package org.apache.lenya.cms.publication;
+
+/**
+ * Document does not exist exception
+ */
+public class DocumentDoesNotExistException extends DocumentException {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+     * Creates a new DocumentDoesNotExistException
+     * 
+     */
+    public DocumentDoesNotExistException() {
+        super();
+    }
+
+    /**
+     * Creates a new DocumentDoesNotExistException
+     * @param message the exception message
+     */
+    public DocumentDoesNotExistException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a new DocumentDoesNotExistException
+     * @param message the exception message
+     * @param cause the cause of the exception
+     */
+    public DocumentDoesNotExistException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Creates a new DocumentDoesNotExistException
+     * @param cause the cause of the exception
+     */
+    public DocumentDoesNotExistException(Throwable cause) {
+        super(cause);
+    }
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentException.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentException.java
new file mode 100644
index 0000000..35ab77d
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentException.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ *
+ */
+
+/* $Id$  */
+
+package org.apache.lenya.cms.publication;
+
+/**
+ * Document exception
+ */
+public class DocumentException extends PublicationException {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+     * Creates a new DocumentException
+     * 
+     */
+    public DocumentException() {
+        super();
+    }
+
+    /**
+     * Creates a new DocumentException
+     * 
+     * @param message the exception message
+     */
+    public DocumentException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a new DocumentException
+     * 
+     * @param message the exception message
+     * @param cause the cause of the exception
+     */
+    public DocumentException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Creates a new DocumentException
+     * 
+     * @param cause the cause of the exception
+     */
+    public DocumentException(Throwable cause) {
+        super(cause);
+    }
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactory.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactory.java
new file mode 100644
index 0000000..048b8ae
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactory.java
@@ -0,0 +1,108 @@
+/*
+ * 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.publication;
+
+/**
+ * A DocumentIdentityMap avoids the multiple instanciation of a document object.
+ * 
+ * @version $Id$
+ */
+public interface DocumentFactory {
+
+    /**
+     * Returns a document.
+     * @param identifier The identifier of the document.
+     * @return A document.
+     * @throws ResourceNotFoundException if the document does not exist.
+     */
+    Document get(DocumentIdentifier identifier) throws ResourceNotFoundException;
+    
+    /**
+     * Returns a document.
+     * @param publication The publication.
+     * @param area The area.
+     * @param uuid The document ID.
+     * @param language The language.
+     * @return A document.
+     * @throws ResourceNotFoundException if the document does not exist.
+     */
+    Document get(Publication publication, String area, String uuid, String language)
+            throws ResourceNotFoundException;
+
+    /**
+     * Returns a revision of a document.
+     * @param publication The publication.
+     * @param area The area.
+     * @param uuid The document ID.
+     * @param language The language.
+     * @param revision The revision..
+     * @return A document.
+     * @throws ResourceNotFoundException if the document does not exist.
+     */
+    Document get(Publication publication, String area, String uuid, String language, int revision)
+            throws ResourceNotFoundException;
+
+    /**
+     * Returns the document identified by a certain web application URL.
+     * @param webappUrl The web application URL.
+     * @return A document.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    Document getFromURL(String webappUrl) throws ResourceNotFoundException;
+
+    /**
+     * Builds a document for the default language.
+     * @param publication The publication.
+     * @param area The area.
+     * @param uuid The document UUID.
+     * @return A document.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    Document get(Publication publication, String area, String uuid)
+            throws ResourceNotFoundException;
+
+    /**
+     * Checks if a webapp URL represents a document.
+     * @param webappUrl A web application URL.
+     * @return A boolean value.
+     */
+    boolean isDocument(String webappUrl);
+    
+    /**
+     * @return The session.
+     */
+    Session getSession();
+    
+    /**
+     * @param id The publication ID.
+     * @return A publication.
+     * @throws PublicationException if the publication does not exist.
+     */
+    Publication getPublication(String id) throws PublicationException;
+    
+    /**
+     * @return All publication IDs.
+     */
+    String[] getPublicationIds();
+    
+    /**
+     * @param id The publication ID.
+     * @return If a publication with this ID exists.
+     */
+    boolean existsPublication(String id);
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryBuilder.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryBuilder.java
new file mode 100644
index 0000000..c0f333c
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryBuilder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.publication;
+
+/**
+ * Document factory builder.
+ */
+public interface DocumentFactoryBuilder {
+
+    /**
+     * Creates a new document factory.
+     * @param session The session.
+     * @return A document identity map.
+     */
+    DocumentFactory createDocumentFactory(Session session);
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryBuilderImpl.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryBuilderImpl.java
new file mode 100644
index 0000000..c92610c
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryBuilderImpl.java
@@ -0,0 +1,79 @@
+/*
+ * 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.publication;
+
+import org.apache.cocoon.util.AbstractLogEnabled;
+import org.apache.excalibur.source.SourceResolver;
+import org.apache.lenya.cms.metadata.MetaDataCache;
+import org.apache.lenya.cms.repository.NodeFactory;
+
+/**
+ * Document factory builder implementation.
+ */
+public class DocumentFactoryBuilderImpl extends AbstractLogEnabled implements
+        DocumentFactoryBuilder {
+
+    private PublicationManager pubManager;
+    private MetaDataCache metaDataCache;
+    private SourceResolver sourceResolver;
+    private NodeFactory nodeFactory;
+    private ResourceTypeResolver resourceTypeResolver;
+
+    public DocumentFactory createDocumentFactory(Session session) {
+        DocumentFactoryImpl factory = new DocumentFactoryImpl(session);
+        factory.setMetaDataCache(getMetaDataCache());
+        factory.setPublicationManager(getPublicationManager());
+        factory.setSourceResolver(getSourceResolver());
+        factory.setNodeFactory(this.nodeFactory);
+        factory.setResourceTypeResolver(this.resourceTypeResolver);
+        return factory;
+    }
+
+    public void setPublicationManager(PublicationManager pubManager) {
+        this.pubManager = pubManager;
+    }
+
+    protected PublicationManager getPublicationManager() {
+        return this.pubManager;
+    }
+
+    public void setMetaDataCache(MetaDataCache metaDataCache) {
+        this.metaDataCache = metaDataCache;
+    }
+
+    protected MetaDataCache getMetaDataCache() {
+        return metaDataCache;
+    }
+
+    public SourceResolver getSourceResolver() {
+        return sourceResolver;
+    }
+
+    public void setSourceResolver(SourceResolver sourceResolver) {
+        this.sourceResolver = sourceResolver;
+    }
+
+    public void setNodeFactory(NodeFactory nodeFactory) {
+        this.nodeFactory = nodeFactory;
+    }
+
+    public void setResourceTypeResolver(ResourceTypeResolver resourceTypeResolver) {
+        this.resourceTypeResolver = resourceTypeResolver;
+    }
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryImpl.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryImpl.java
new file mode 100644
index 0000000..4e557aa
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentFactoryImpl.java
@@ -0,0 +1,374 @@
+/*
+ * 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.publication;
+
+import java.util.Arrays;
+import java.util.StringTokenizer;
+
+import org.apache.commons.lang.Validate;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.excalibur.source.SourceResolver;
+import org.apache.lenya.cms.metadata.MetaDataCache;
+import org.apache.lenya.cms.repository.NodeFactory;
+import org.apache.lenya.cms.repository.RepositoryException;
+import org.apache.lenya.cms.repository.RepositoryItem;
+import org.apache.lenya.cms.repository.RepositoryItemFactory;
+import org.apache.lenya.cms.repository.SessionHolder;
+
+/**
+ * A DocumentIdentityMap avoids the multiple instanciation of a document object.
+ * 
+ * @version $Id: DocumentIdentityMap.java 264153 2005-08-29 15:11:14Z andreas $
+ */
+public class DocumentFactoryImpl implements DocumentFactory, RepositoryItemFactory {
+
+    private static final Log logger = LogFactory.getLog(DocumentFactoryImpl.class);
+
+    private Session session;
+    private MetaDataCache metaDataCache;
+    private SourceResolver sourceResolver;
+    private NodeFactory nodeFactory;
+    private ResourceTypeResolver resourceTypeResolver;
+    
+    /**
+     * @return The session.
+     */
+    public Session getSession() {
+        return this.session;
+    }
+
+    /**
+     * Ctor.
+     * @param session The session to use.
+     */
+    public DocumentFactoryImpl(Session session) {
+        this.session = session;
+    }
+
+    /**
+     * Returns a document.
+     * @param publication The publication.
+     * @param area The area.
+     * @param uuid The document UUID.
+     * @param language The language.
+     * @return A document.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    public Document get(Publication publication, String area, String uuid, String language)
+            throws ResourceNotFoundException {
+        return get(publication, area, uuid, language, -1);
+    }
+
+    public Document get(Publication publication, String area, String uuid, String language,
+            int revision) throws ResourceNotFoundException {
+        if (logger.isDebugEnabled())
+            logger.debug(
+                    "DocumentIdentityMap::get() called on publication [" + publication.getId()
+                            + "], area [" + area + "], UUID [" + uuid + "], language [" + language
+                            + "]");
+
+        String key = getKey(publication, area, uuid, language, revision);
+
+        if (logger.isDebugEnabled())
+            logger.debug(
+                    "DocumentIdentityMap::get() got key [" + key + "] from DocumentFactory");
+
+        try {
+            return (Document) getRepositorySession().getRepositoryItem(this, key);
+        } catch (RepositoryException e) {
+            throw new ResourceNotFoundException(e);
+        }
+    }
+
+    protected org.apache.lenya.cms.repository.Session getRepositorySession() {
+        SessionHolder holder = (SessionHolder) this.session;
+        return holder.getRepositorySession();
+    }
+
+    /**
+     * Returns the document identified by a certain web application URL.
+     * @param webappUrl The web application URL.
+     * @return A document.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    public Document getFromURL(String webappUrl) throws ResourceNotFoundException {
+        String key = getKey(webappUrl);
+        try {
+            return (Document) getRepositorySession().getRepositoryItem(this, key);
+        } catch (RepositoryException e) {
+            throw new ResourceNotFoundException(e);
+        }
+    }
+
+    /**
+     * Builds a clone of a document for another language.
+     * @param document The document to clone.
+     * @param language The language of the target document.
+     * @return A document.
+     * @throws DocumentBuildException if an error occurs.
+     */
+    public Document getLanguageVersion(Document document, String language)
+            throws DocumentBuildException {
+        return get(document.getPublication(), document.getArea(), document.getUUID(), language);
+    }
+
+    /**
+     * Builds a clone of a document for another area.
+     * @param document The document to clone.
+     * @param area The area of the target document.
+     * @return A document.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    public Document getAreaVersion(Document document, String area) throws ResourceNotFoundException {
+        return get(document.getPublication(), area, document.getUUID(), document.getLanguage());
+    }
+
+    /**
+     * Builds a document for the default language.
+     * @param publication The publication.
+     * @param area The area.
+     * @param documentId The document ID.
+     * @return A document.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    public Document get(Publication publication, String area, String documentId)
+            throws ResourceNotFoundException {
+        return get(publication, area, documentId, publication.getDefaultLanguage());
+    }
+
+    /**
+     * Checks if a string represents a valid document ID.
+     * @param id The string.
+     * @return A boolean value.
+     */
+    public boolean isValidDocumentId(String id) {
+
+        if (!id.startsWith("/")) {
+            return false;
+        }
+
+        String[] snippets = id.split("/");
+
+        if (snippets.length < 2) {
+            return false;
+        }
+
+        for (int i = 1; i < snippets.length; i++) {
+            if (!snippets[i].matches("[a-zA-Z0-9\\-]+")) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if a webapp URL represents a document.
+     * @param webappUrl A web application URL.
+     * @return A boolean value.
+     * @throws ResourceNotFoundException if an error occurs.
+     */
+    public boolean isDocument(String webappUrl) throws ResourceNotFoundException {
+        Validate.notNull(webappUrl);
+        try {
+            URLInformation info = new URLInformation(webappUrl);
+            String pubId = info.getPublicationId();
+            String[] pubIds = getPublicationIds();
+            if (pubId != null && Arrays.asList(pubIds).contains(pubId)) {
+                Publication pub = getPublication(pubId);
+                DocumentBuilder builder = pub.getDocumentBuilder();
+                return builder.isDocument(this.session, webappUrl);
+            } else {
+                return false;
+            }
+        } catch (PublicationException e) {
+            throw new ResourceNotFoundException(e);
+        }
+    }
+
+    /**
+     * Builds a document key.
+     * @param publication The publication.
+     * @param area The area.
+     * @param uuid The document UUID.
+     * @param language The language.
+     * @param revision
+     * @return A key.
+     */
+    public String getKey(Publication publication, String area, String uuid, String language,
+            int revision) {
+        Validate.notNull(publication);
+        Validate.notNull(area);
+        Validate.notNull(uuid);
+        Validate.notNull(language);
+        return publication.getId() + ":" + area + ":" + uuid + ":" + language + ":" + revision;
+    }
+
+    /**
+     * Builds a document key.
+     * @param webappUrl The web application URL.
+     * @return A key.
+     */
+    public String getKey(String webappUrl) {
+        Validate.notNull(webappUrl);
+        try {
+            if (!isDocument(webappUrl)) {
+                throw new RuntimeException("No document for URL [" + webappUrl + "] found.");
+            }
+            DocumentLocator locator = getLocator(webappUrl);
+            Publication publication = getPublication(locator.getPublicationId());
+            String area = locator.getArea();
+            String uuid = publication.getArea(area).getSite().getNode(locator.getPath()).getUuid();
+            return getKey(publication, area, uuid, locator.getLanguage(), -1);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected DocumentLocator getLocator(String webappUrl) {
+        DocumentLocator locator;
+        try {
+            URLInformation info = new URLInformation(webappUrl);
+            Publication publication = getPublication(info.getPublicationId());
+            DocumentBuilder builder = publication.getDocumentBuilder();
+            locator = builder.getLocator(this.session, webappUrl);
+
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return locator;
+    }
+
+    /**
+     * @see org.apache.lenya.transaction.IdentifiableFactory#build(org.apache.lenya.transaction.IdentityMap,
+     *      java.lang.String)
+     */
+    public RepositoryItem buildItem(org.apache.lenya.cms.repository.Session session, String key) throws RepositoryException {
+        if (logger.isDebugEnabled())
+            logger.debug("DocumentFactory::build() called with key [" + key + "]");
+
+        StringTokenizer tokenizer = new StringTokenizer(key, ":");
+        String publicationId = tokenizer.nextToken();
+        String area = tokenizer.nextToken();
+        String uuid = tokenizer.nextToken();
+        String language = tokenizer.nextToken();
+        String revisionString = tokenizer.nextToken();
+        int revision = Integer.valueOf(revisionString).intValue();
+
+        DocumentImpl document;
+        try {
+            Publication publication = getPublication(publicationId);
+            DocumentBuilder builder = publication.getDocumentBuilder();
+            DocumentIdentifier identifier = new DocumentIdentifier(publicationId, area, uuid,
+                    language);
+            document = buildDocument(identifier, revision, builder);
+        } catch (Exception e) {
+            throw new RepositoryException(e);
+        }
+        if (logger.isDebugEnabled())
+            logger.debug("DocumentFactory::build() done.");
+
+        return document;
+    }
+
+    protected DocumentImpl buildDocument(DocumentIdentifier identifier, int revision,
+            DocumentBuilder builder) throws DocumentBuildException {
+        return createDocument(identifier, revision, builder);
+    }
+
+    /**
+     * Creates a new document object. Override this method to create specific document objects,
+     * e.g., for different document IDs.
+     * @param identifier The identifier.
+     * @param revision The revision or -1 for the latest revision.
+     * @param builder The document builder.
+     * @return A document.
+     * @throws DocumentBuildException when something went wrong.
+     */
+    protected DocumentImpl createDocument(DocumentIdentifier identifier, int revision,
+            DocumentBuilder builder) throws DocumentBuildException {
+        DocumentImpl doc = new DocumentImpl(session, identifier, revision);
+        doc.setMetaDataCache(getMetaDataCache());
+        doc.setSourceResolver(getSourceResolver());
+        doc.setNodeFactory(this.nodeFactory);
+        doc.setResourceTypeResolver(this.resourceTypeResolver);
+        return doc;
+    }
+
+    public Document get(DocumentIdentifier identifier) throws ResourceNotFoundException {
+        try {
+            Publication pub = getPublication(identifier.getPublicationId());
+            return get(pub, identifier.getArea(), identifier.getUUID(), identifier.getLanguage());
+        } catch (PublicationException e) {
+            throw new ResourceNotFoundException(e);
+        }
+    }
+
+    public String getItemType() {
+        return Document.TRANSACTIONABLE_TYPE;
+    }
+
+    public Publication getPublication(String id) throws PublicationException {
+        return getPublicationManager().getPublication(this, id);
+    }
+
+    public String[] getPublicationIds() {
+        return getPublicationManager().getPublicationIds();
+    }
+
+    private PublicationManager pubManager;
+
+    protected void setPublicationManager(PublicationManager pubManager) {
+        this.pubManager = pubManager;
+    }
+
+    protected PublicationManager getPublicationManager() {
+        return this.pubManager;
+    }
+
+    public boolean existsPublication(String id) {
+        return Arrays.asList(getPublicationManager().getPublicationIds()).contains(id);
+    }
+
+    protected MetaDataCache getMetaDataCache() {
+        return metaDataCache;
+    }
+
+    public void setMetaDataCache(MetaDataCache metaDataCache) {
+        this.metaDataCache = metaDataCache;
+    }
+
+    public SourceResolver getSourceResolver() {
+        return sourceResolver;
+    }
+
+    public void setSourceResolver(SourceResolver sourceResolver) {
+        this.sourceResolver = sourceResolver;
+    }
+
+    public void setNodeFactory(NodeFactory nodeFactory) {
+        Validate.notNull(nodeFactory, "node factory");
+        this.nodeFactory = nodeFactory;
+    }
+
+    public void setResourceTypeResolver(ResourceTypeResolver resourceTypeResolver) {
+        this.resourceTypeResolver = resourceTypeResolver;
+    }
+
+}
\ No newline at end of file
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentIdToPathMapper.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentIdToPathMapper.java
new file mode 100644
index 0000000..43fee73
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentIdToPathMapper.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ *
+ */
+
+/* $Id$  */
+
+package org.apache.lenya.cms.publication;
+
+/**
+ * Document Id to Path mapper interface
+ */
+public interface DocumentIdToPathMapper {
+
+    /**
+     * Compute the document-path for a given publication, area and document-uuid. The file separator
+     * is the slash (/).
+     * 
+     * @param uuid the UUID of the document
+     * @param language the language of the document
+     * 
+     * @return the path to the document, without publication ID and area
+     */
+    String getPath(String uuid, String language);
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentIdentifier.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentIdentifier.java
new file mode 100644
index 0000000..38cac3e
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentIdentifier.java
@@ -0,0 +1,97 @@
+/*
+ * 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.publication;
+
+/**
+ * Value object to identify documents.
+ */
+public class DocumentIdentifier {
+
+    private String publicationId;
+    private String area;
+    private String language;
+    private String uuid;
+
+    /**
+     * Ctor.
+     * @param pubId The publication ID.
+     * @param area The area.
+     * @param uuid The document UUID.
+     * @param language The language.
+     */
+    public DocumentIdentifier(String pubId, String area, String uuid, String language) {
+
+        if (uuid.startsWith("/") && uuid.split("-").length == 4) {
+            throw new IllegalArgumentException("The UUID [" + uuid + "] must not begin with a '/'!");
+        }
+        if (uuid.indexOf("/") > 0) {
+            throw new IllegalArgumentException("The UUID [" + uuid
+                    + "] must not contain a '/' after the first position!");
+        }
+
+        this.publicationId = pubId;
+        this.area = area;
+        this.language = language;
+        this.uuid = uuid;
+    }
+
+    /**
+     * @return The UUID.
+     */
+    public String getUUID() {
+        return this.uuid;
+    }
+
+    /**
+     * @return The area.
+     */
+    public String getArea() {
+        return area;
+    }
+
+    /**
+     * @return The language.
+     */
+    public String getLanguage() {
+        return language;
+    }
+
+    /**
+     * @return The publication ID.
+     */
+    public String getPublicationId() {
+        return publicationId;
+    }
+
+    public boolean equals(Object obj) {
+        return (obj instanceof DocumentIdentifier) && obj.hashCode() == hashCode();
+    }
+
+    public int hashCode() {
+        return getKey().hashCode();
+    }
+
+    protected String getKey() {
+        return this.publicationId + ":" + this.area + ":" + this.uuid + ":" + this.language;
+    }
+
+    public String toString() {
+        return getKey();
+    }
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentImpl.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentImpl.java
new file mode 100644
index 0000000..282b510
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentImpl.java
@@ -0,0 +1,779 @@
+/*
+ * 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.publication;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.lang.Validate;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.excalibur.source.SourceResolver;
+import org.apache.lenya.cms.cocoon.source.SourceUtil;
+import org.apache.lenya.cms.metadata.MetaData;
+import org.apache.lenya.cms.metadata.MetaDataCache;
+import org.apache.lenya.cms.metadata.MetaDataException;
+import org.apache.lenya.cms.metadata.MetaDataWrapper;
+import org.apache.lenya.cms.publication.util.DocumentVisitor;
+import org.apache.lenya.cms.repository.ContentHolder;
+import org.apache.lenya.cms.repository.Node;
+import org.apache.lenya.cms.repository.NodeFactory;
+import org.apache.lenya.cms.repository.RepositoryItem;
+import org.apache.lenya.cms.repository.Session;
+import org.apache.lenya.cms.repository.SessionHolder;
+import org.apache.lenya.cms.site.Link;
+import org.apache.lenya.cms.site.SiteException;
+import org.apache.lenya.cms.site.SiteStructure;
+
+/**
+ * A typical CMS document.
+ * @version $Id$
+ */
+public class DocumentImpl implements Document, RepositoryItem {
+
+    private static final Log logger = LogFactory.getLog(DocumentImpl.class);
+
+    private DocumentIdentifier identifier;
+    private org.apache.lenya.cms.publication.Session session;
+    private NodeFactory nodeFactory;
+    private ResourceTypeResolver resourceTypeResolver;
+    private int revision = -1;
+
+    /**
+     * The meta data namespace.
+     */
+    public static final String METADATA_NAMESPACE = "http://apache.org/lenya/metadata/document/1.0";
+
+    /**
+     * The name of the resource type attribute. A resource has a resource type; this information can
+     * be used e.g. for different rendering of different types.
+     */
+    protected static final String METADATA_RESOURCE_TYPE = "resourceType";
+
+    /**
+     * The name of the mime type attribute.
+     */
+    protected static final String METADATA_MIME_TYPE = "mimeType";
+
+    /**
+     * The name of the content type attribute. Any content managed by Lenya has a type; this
+     * information can be used e.g. to provide an appropriate management interface.
+     */
+    protected static final String METADATA_CONTENT_TYPE = "contentType";
+
+    /**
+     * The number of seconds from the request that a document can be cached before it expires
+     */
+    protected static final String METADATA_EXPIRES = "expires";
+
+    /**
+     * The extension to use for the document source.
+     */
+    protected static final String METADATA_EXTENSION = "extension";
+
+    /**
+     * Creates a new instance of DefaultDocument.
+     * @param session The session the document belongs to.
+     * @param identifier The identifier.
+     * @param revision The revision number or -1 if the latest revision should be used.
+     */
+    protected DocumentImpl(org.apache.lenya.cms.publication.Session session,
+            DocumentIdentifier identifier, int revision) {
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("DefaultDocument() creating new instance with id [" + identifier.getUUID()
+                    + "], language [" + identifier.getLanguage() + "]");
+        }
+
+        if (identifier.getUUID() == null) {
+            throw new IllegalArgumentException("The UUID must not be null!");
+        }
+
+        this.identifier = identifier;
+        this.session = session;
+        this.revision = revision;
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("DefaultDocument() done building instance with _id ["
+                    + identifier.getUUID() + "], _language [" + identifier.getLanguage() + "]");
+        }
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getExpires()
+     */
+    public Date getExpires() throws DocumentException {
+        Date expires = null;
+        long secs = 0;
+
+        MetaData metaData = null;
+        String expiresMeta = null;
+        try {
+            metaData = this.getMetaData(METADATA_NAMESPACE);
+            expiresMeta = metaData.getFirstValue("expires");
+        } catch (MetaDataException e) {
+            throw new DocumentException(e);
+        }
+        if (expiresMeta != null) {
+            secs = Long.parseLong(expiresMeta);
+        } else {
+            secs = -1;
+        }
+
+        if (secs != -1) {
+            Date date = new Date();
+            date.setTime(date.getTime() + secs * 1000l);
+            expires = date;
+        } else {
+            expires = this.getResourceType().getExpires();
+        }
+
+        return expires;
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getName()
+     */
+    public String getName() {
+        try {
+            return getLink().getNode().getName();
+        } catch (DocumentException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Publication publication;
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getPublication()
+     */
+    public Publication getPublication() {
+        if (this.publication == null) {
+            this.publication = getSession().getPublication(getIdentifier().getPublicationId());
+        }
+        return this.publication;
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getLastModified()
+     */
+    public long getLastModified() throws DocumentException {
+        try {
+            return getRepositoryNode().getLastModified();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new DocumentException(e);
+        }
+    }
+
+    public String getLanguage() {
+        return this.identifier.getLanguage();
+    }
+
+    public String[] getLanguages() {
+
+        List documentLanguages = new ArrayList();
+        String[] allLanguages = getPublication().getLanguages();
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Number of languages of this publication: " + allLanguages.length);
+        }
+
+        for (int i = 0; i < allLanguages.length; i++) {
+            if (existsTranslation(allLanguages[i])) {
+                documentLanguages.add(allLanguages[i]);
+            }
+        }
+
+        return (String[]) documentLanguages.toArray(new String[documentLanguages.size()]);
+    }
+
+    public String getArea() {
+        return this.identifier.getArea();
+    }
+
+    private String extension = null;
+    private String defaultExtension = "html";
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getExtension()
+     */
+    public String getExtension() {
+        if (extension == null) {
+            String sourceExtension = getSourceExtension();
+            if (sourceExtension.equals("xml") || sourceExtension.equals("")) {
+                logger.info("Default extension will be used: " + defaultExtension);
+                return defaultExtension;
+            } else {
+                return sourceExtension;
+            }
+
+        }
+        return this.extension;
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getUUID()
+     */
+    public String getUUID() {
+        return getIdentifier().getUUID();
+    }
+
+    private String defaultSourceExtension = "xml";
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getSourceExtension()
+     */
+    public String getSourceExtension() {
+        String sourceExtension;
+        try {
+            sourceExtension = getMetaData(METADATA_NAMESPACE).getFirstValue(METADATA_EXTENSION);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        if (sourceExtension == null) {
+            logger.warn("No source extension for document [" + this + "]. The extension \""
+                    + defaultSourceExtension + "\" will be used as default!");
+            sourceExtension = defaultSourceExtension;
+        }
+        return sourceExtension;
+    }
+
+    /**
+     * Sets the extension of the file in the URL.
+     * @param _extension A string.
+     */
+    protected void setExtension(String _extension) {
+        Validate.notNull(_extension);
+        Validate.isTrue(!_extension.startsWith("."), "Extension must start with a dot");
+        checkWritability();
+        this.extension = _extension;
+    }
+
+    public boolean exists() throws ResourceNotFoundException {
+        try {
+            return getRepositoryNode().exists();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new ResourceNotFoundException(e);
+        }
+    }
+
+    public boolean existsInAnyLanguage() throws ResourceNotFoundException {
+        String[] languages = getLanguages();
+
+        if (languages.length > 0) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Document (" + this + ") exists in at least one language: "
+                        + languages.length);
+            }
+            String[] allLanguages = getPublication().getLanguages();
+            if (languages.length == allLanguages.length)
+                // TODO: This is not entirely true, because the publication
+                // could assume the
+                // languages EN and DE, but the document could exist for the
+                // languages DE and FR!
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Document (" + this
+                            + ") exists even in all languages of this publication");
+                }
+            return true;
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Document (" + this + ") does NOT exist in any language");
+            }
+            return false;
+        }
+
+    }
+
+    public DocumentIdentifier getIdentifier() {
+        return this.identifier;
+    }
+
+    /**
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    public boolean equals(Object object) {
+        if (getClass().isInstance(object)) {
+            DocumentImpl document = (DocumentImpl) object;
+            return document.getIdentifier().equals(getIdentifier());
+        }
+        return false;
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     */
+    public int hashCode() {
+
+        String key = getPublication().getId() + ":" + getPublication().getPubBaseUri() + ":"
+                + getArea() + ":" + getUUID() + ":" + getLanguage();
+
+        return key.hashCode();
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    public String toString() {
+        return getIdentifier().toString();
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getCanonicalWebappURL()
+     */
+    public String getCanonicalWebappURL() {
+        return "/" + getPublication().getId() + "/" + getArea() + getCanonicalDocumentURL();
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getCanonicalDocumentURL()
+     */
+    public String getCanonicalDocumentURL() {
+        try {
+            DocumentBuilder builder = getPublication().getDocumentBuilder();
+            String webappUrl = builder.buildCanonicalUrl(getSession(), getLocator());
+            String prefix = "/" + getPublication().getId() + "/" + getArea();
+            return webappUrl.substring(prefix.length());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public org.apache.lenya.cms.publication.Session getSession() {
+        return this.session;
+    }
+
+    public void accept(DocumentVisitor visitor) throws Exception {
+        visitor.visitDocument(this);
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#delete()
+     */
+    public void delete() throws DocumentException {
+        if (hasLink()) {
+            throw new DocumentException("Can't delete document [" + this
+                    + "], it's still referenced in the site structure.");
+        }
+        try {
+            getRepositoryNode().delete();
+        } catch (Exception e) {
+            throw new DocumentException(e);
+        }
+    }
+
+    protected static final String IDENTIFIABLE_TYPE = "document";
+
+    private ResourceType resourceType;
+    private MetaDataCache metaDataCache;
+    private SourceResolver resolver;
+
+    /**
+     * Convenience method to read the document's resource type from the meta-data.
+     * @see Document#getResourceType()
+     */
+    public ResourceType getResourceType() throws DocumentException {
+        if (this.resourceType == null) {
+            String name;
+            try {
+                name = getMetaData(METADATA_NAMESPACE).getFirstValue(METADATA_RESOURCE_TYPE);
+            } catch (MetaDataException e) {
+                throw new DocumentException(e);
+            }
+            if (name == null) {
+                throw new DocumentException("No resource type defined for document [" + this + "]!");
+            }
+            this.resourceType = this.resourceTypeResolver.getResourceType(name);
+        }
+        return this.resourceType;
+    }
+
+    public MetaData getMetaData(String namespaceUri) throws MetaDataException {
+        MetaData meta;
+        try {
+            meta = new MetaDataWrapper(getContentHolder().getMetaData(namespaceUri));
+        } catch (org.apache.lenya.cms.repository.metadata.MetaDataException e) {
+            throw new MetaDataException(e);
+        }
+        if (getRepositorySession().isModifiable()) {
+            return meta;
+        } else {
+            String cacheKey = getPublication().getId() + ":" + getArea() + ":" + getUUID() + ":"
+                    + getLanguage();
+            return getMetaDataCache().getMetaData(cacheKey, meta, namespaceUri);
+        }
+    }
+
+    protected MetaDataCache getMetaDataCache() {
+        return this.metaDataCache;
+    }
+
+    public String[] getMetaDataNamespaceUris() throws MetaDataException {
+        try {
+            return getContentHolder().getMetaDataNamespaceUris();
+        } catch (org.apache.lenya.cms.repository.metadata.MetaDataException e) {
+            throw new MetaDataException(e);
+        }
+    }
+
+    public String getMimeType() throws DocumentException {
+        try {
+            String mimeType = getMetaData(METADATA_NAMESPACE).getFirstValue(METADATA_MIME_TYPE);
+            if (mimeType == null) {
+                mimeType = "";
+            }
+            return mimeType;
+        } catch (MetaDataException e) {
+            throw new DocumentException(e);
+        }
+    }
+
+    public long getContentLength() {
+        try {
+            return getContentHolder().getContentLength();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void setMimeType(String mimeType) {
+        checkWritability();
+        try {
+            getMetaData(METADATA_NAMESPACE).setValue(METADATA_MIME_TYPE, mimeType);
+        } catch (MetaDataException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public DocumentLocator getLocator() {
+        SiteStructure structure = area().getSite();
+        if (!structure.containsByUuid(getUUID(), getLanguage())) {
+            throw new RuntimeException("The document [" + this
+                    + "] is not referenced in the site structure.");
+        }
+        try {
+            return DocumentLocator.getLocator(getPublication().getId(), getArea(), structure
+                    .getByUuid(getUUID(), getLanguage()).getNode().getPath(), getLanguage());
+        } catch (SiteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public String getPath() throws DocumentException {
+        return getLink().getNode().getPath();
+    }
+
+    public boolean existsAreaVersion(String area) {
+        String sourceUri = getSourceURI(getPublication(), area, getUUID(), getLanguage());
+        try {
+            return SourceUtil.exists(sourceUri, this.resolver);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean existsTranslation(String language) {
+        return area().contains(getUUID(), language);
+    }
+
+    public Document getAreaVersion(String area) throws ResourceNotFoundException {
+        return getPublication().getArea(area).getDocument(getUUID(), getLanguage());
+    }
+
+    public Document getTranslation(String language) throws ResourceNotFoundException {
+        return area().getDocument(getUUID(), language);
+    }
+
+    private Node repositoryNode;
+
+    public Node getRepositoryNode() {
+        if (this.repositoryNode == null) {
+            SessionHolder holder = (SessionHolder) getSession();
+            this.repositoryNode = getRepositoryNode(getNodeFactory(),
+                    holder.getRepositorySession(), getSourceURI());
+        }
+        return this.repositoryNode;
+    }
+
+    protected ContentHolder getContentHolder() {
+        Node node = getRepositoryNode();
+        if (isRevisionSpecified()) {
+            try {
+                return node.getHistory().getRevision(revision);
+            } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            return node;
+        }
+    }
+
+    protected static Node getRepositoryNode(NodeFactory nodeFactory, Session session,
+            String sourceUri) {
+        try {
+            return (Node) session.getRepositoryItem(nodeFactory, sourceUri);
+        } catch (Exception e) {
+            throw new RuntimeException("Creating repository node failed: ", e);
+        }
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.Document#getSourceURI()
+     */
+    public String getSourceURI() {
+        return getSourceURI(getPublication(), getArea(), getUUID(), getLanguage());
+    }
+
+    protected static String getSourceURI(Publication pub, String area, String uuid, String language) {
+        String path = pub.getPathMapper().getPath(uuid, language);
+        return pub.getContentUri(area) + "/" + path;
+    }
+
+    public boolean existsVersion(String area, String language) {
+        String sourceUri = getSourceURI(getPublication(), area, getUUID(), language);
+        try {
+            return SourceUtil.exists(sourceUri, getSourceResolver());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Document getVersion(String area, String language) throws ResourceNotFoundException {
+        return getPublication().getArea(area).getDocument(getUUID(), language);
+    }
+
+    public Link getLink() throws DocumentException {
+        SiteStructure structure = area().getSite();
+        try {
+            if (structure.containsByUuid(getUUID(), getLanguage())) {
+                return structure.getByUuid(getUUID(), getLanguage());
+            } else {
+                throw new DocumentException("The document [" + this
+                        + "] is not referenced in the site structure [" + structure + "].");
+            }
+        } catch (Exception e) {
+            throw new DocumentException(e);
+        }
+    }
+
+    public boolean hasLink() {
+        return area().getSite().containsByUuid(getUUID(), getLanguage());
+    }
+
+    public Area area() {
+        return getPublication().getArea(getArea());
+    }
+
+    public void setResourceType(ResourceType resourceType) {
+        Validate.notNull(resourceType);
+        checkWritability();
+        try {
+            MetaData meta = getMetaData(DocumentImpl.METADATA_NAMESPACE);
+            meta.setValue(DocumentImpl.METADATA_RESOURCE_TYPE, resourceType.getName());
+        } catch (MetaDataException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void setSourceExtension(String extension) {
+        Validate.notNull(extension);
+        Validate.isTrue(!extension.startsWith("."), "Extension must start with a dot");
+        checkWritability();
+        try {
+            MetaData meta = getMetaData(DocumentImpl.METADATA_NAMESPACE);
+            meta.setValue(DocumentImpl.METADATA_EXTENSION, extension);
+        } catch (MetaDataException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public OutputStream getOutputStream() {
+        checkWritability();
+        try {
+            return getRepositoryNode().getOutputStream();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected void checkWritability() {
+        if (isRevisionSpecified()) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    protected boolean isRevisionSpecified() {
+        return this.revision != -1;
+    }
+
+    public InputStream getInputStream() {
+        try {
+            return getRepositoryNode().getInputStream();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Session getRepositorySession() {
+        return ((SessionHolder) getSession()).getRepositorySession();
+    }
+
+    public int getRevisionNumber() {
+        if (!isRevisionSpecified()) {
+            throw new UnsupportedOperationException(
+                    "This is not a particular revision of the document [" + this + "].");
+        }
+        return this.revision;
+    }
+
+    public void setMetaDataCache(MetaDataCache metaDataCache) {
+        this.metaDataCache = metaDataCache;
+    }
+
+    public SourceResolver getSourceResolver() {
+        return resolver;
+    }
+
+    public void setSourceResolver(SourceResolver resolver) {
+        this.resolver = resolver;
+    }
+
+    public NodeFactory getNodeFactory() {
+        return nodeFactory;
+    }
+
+    public void setNodeFactory(NodeFactory nodeFactory) {
+        this.nodeFactory = nodeFactory;
+    }
+
+    public void checkin() throws RepositoryException {
+        try {
+            getRepositoryNode().checkin();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public boolean isCheckedOutBySession(String sessionId, String userId)
+            throws RepositoryException {
+        try {
+            return getRepositoryNode().isCheckedOutBySession(sessionId, userId);
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void checkout() throws RepositoryException {
+        try {
+            getRepositoryNode().checkout();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public String getCheckoutUserId() throws RepositoryException {
+        try {
+            return getRepositoryNode().getCheckoutUserId();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public boolean isCheckedOut() throws RepositoryException {
+        try {
+            return getRepositoryNode().isCheckedOut();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void lock() throws RepositoryException {
+        try {
+            getRepositoryNode().lock();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void registerDirty() throws RepositoryException {
+        try {
+            getRepositoryNode().registerDirty();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void unlock() throws RepositoryException {
+        try {
+            getRepositoryNode().unlock();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    private History history;
+
+    public History getHistory() {
+        if (this.history == null) {
+            this.history = new HistoryWrapper(getRepositoryNode().getHistory());
+        }
+        return this.history;
+    }
+
+    public boolean isLocked() {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public Document getRevision(int i) {
+        return area().getDocument(getUUID(), getLanguage(), i);
+    }
+
+    public void forceCheckIn() throws RepositoryException {
+        try {
+            getRepositoryNode().forceCheckIn();
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void rollback(int revision) throws RepositoryException {
+        try {
+            getRepositoryNode().rollback(revision);
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void checkout(boolean checkoutRestrictedToSession) throws RepositoryException {
+        try {
+            getRepositoryNode().checkout(checkoutRestrictedToSession);
+        } catch (org.apache.lenya.cms.repository.RepositoryException e) {
+            throw new RepositoryException(e);
+        }
+    }
+
+    public void setResourceTypeResolver(ResourceTypeResolver resourceTypeResolver) {
+        this.resourceTypeResolver = resourceTypeResolver;
+    }
+
+}
\ No newline at end of file
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentLocator.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentLocator.java
new file mode 100644
index 0000000..c215ddf
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentLocator.java
@@ -0,0 +1,210 @@
+/*
+ * 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.publication;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+
+/**
+ * A DocumentLocator describes a document based on its path in the site structure. The actual
+ * document doesn't have to exist.
+ */
+public class DocumentLocator {
+
+    private static Map locators = new WeakHashMap();
+
+    /**
+     * Returns a specific document locator.
+     * @param pubId The publication ID.
+     * @param area The area of the document.
+     * @param path The path of the document in the site structure.
+     * @param language The language of the document.
+     * @return A document locator.
+     */
+    public static DocumentLocator getLocator(String pubId, String area, String path, String language) {
+        String key = DocumentLocator.getKey(pubId, area, path, language);
+        DocumentLocator locator = (DocumentLocator) locators.get(key);
+        if (locator == null) {
+            locator = new DocumentLocator(pubId, area, path, language);
+            locators.put(key, locator);
+        }
+        return locator;
+    }
+
+    protected static final String getKey(String pubId, String area, String path, String language) {
+        return pubId + ":" + area + ":" + path + ":" + language;
+    }
+
+    private String pubId;
+    private String area;
+    private String path;
+    private String language;
+
+    protected DocumentLocator(String pubId, String area, String path, String language) {
+        this.path = path;
+        this.pubId = pubId;
+        this.area = area;
+        this.language = language;
+    }
+
+    /**
+     * @return The area of the document.
+     */
+    public String getArea() {
+        return area;
+    }
+
+    /**
+     * @return The language of the document.
+     */
+    public String getLanguage() {
+        return language;
+    }
+
+    /**
+     * @return The path of the document in the site structure.
+     */
+    public String getPath() {
+        return path;
+    }
+
+    /**
+     * @return The publication ID.
+     */
+    public String getPublicationId() {
+        return pubId;
+    }
+
+    /**
+     * Returns a locator with the same publication ID, area, and language, but a different path in
+     * the site structure.
+     * @param path The path.
+     * @return A document locator.
+     */
+    public DocumentLocator getPathVersion(String path) {
+        return DocumentLocator.getLocator(getPublicationId(), getArea(), path, getLanguage());
+    }
+
+    /**
+     * Returns a descendant of this locator.
+     * @param relativePath The relative path which must not begin with a slash and must not be
+     *            empty.
+     * @return A document locator.
+     */
+    public DocumentLocator getDescendant(String relativePath) {
+        if (relativePath.length() == 0) {
+            throw new IllegalArgumentException("The relative path must not be empty!");
+        }
+        if (relativePath.startsWith("/")) {
+            throw new IllegalArgumentException("The relative path must not start with a slash!");
+        }
+        return getPathVersion(getPath() + "/" + relativePath);
+    }
+
+    /**
+     * Returns a child of this locator.
+     * @param step The relative path to the child, it must not contain a slash.
+     * @return A document locator.
+     */
+    public DocumentLocator getChild(String step) {
+        if (step.indexOf("/") > -1) {
+            throw new IllegalArgumentException("The step [" + step + "] must not contain a slash!");
+        }
+        return getDescendant(step);
+    }
+
+    /**
+     * Returns the parent of this locator.
+     * @return A document locator or <code>null</code> if this is the root locator.
+     */
+    public DocumentLocator getParent() {
+        int lastSlashIndex = getPath().lastIndexOf("/");
+        if (lastSlashIndex > -1) {
+            String parentPath = getPath().substring(0, lastSlashIndex);
+            return getPathVersion(parentPath);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the parent of this locator.
+     * @param defaultPath The path of the locator to return if this is the root locator.
+     * @return A document locator.
+     */
+    public DocumentLocator getParent(String defaultPath) {
+        DocumentLocator parent = getParent();
+        if (parent != null) {
+            return parent;
+        } else {
+            return getPathVersion(defaultPath);
+        }
+    }
+
+    /**
+     * Returns a locator with the same publication ID, area, and path, but with a different
+     * language.
+     * @param language The language.
+     * @return A document locator.
+     */
+    public DocumentLocator getLanguageVersion(String language) {
+        return DocumentLocator.getLocator(getPublicationId(), getArea(), getPath(), language);
+    }
+
+    protected String getKey() {
+        return DocumentLocator.getKey(getPublicationId(), getArea(), getPath(), getLanguage());
+    }
+
+    public boolean equals(Object obj) {
+        if (!(obj instanceof DocumentLocator)) {
+            return false;
+        }
+        DocumentLocator locator = (DocumentLocator) obj;
+        return locator.getKey().equals(getKey());
+    }
+
+    public int hashCode() {
+        return getKey().hashCode();
+    }
+
+    public String toString() {
+        return getKey();
+    }
+
+    /**
+     * Returns a locator with the same publication ID, path, and language, but with a different
+     * area.
+     * @param area The area.
+     * @return A document locator.
+     */
+    public DocumentLocator getAreaVersion(String area) {
+        return DocumentLocator.getLocator(getPublicationId(), area, getPath(), getLanguage());
+    }
+
+    public Document getDocument(Session session) throws ResourceNotFoundException {
+        try {
+            Publication pub = session.getPublication(getPublicationId());
+            return pub.getArea(getArea()).getSite().getNode(getPath()).getLink(getLanguage())
+                    .getDocument();
+        } catch (PublicationException e) {
+            throw new ResourceNotFoundException(e);
+        }
+    }
+
+}
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentManager.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentManager.java
new file mode 100644
index 0000000..77fab08
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentManager.java
@@ -0,0 +1,252 @@
+/*
+ * 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.publication;
+
+import org.apache.lenya.cms.publication.util.DocumentSet;
+
+/**
+ * Helper to manage documents. It takes care of attachments etc.
+ * 
+ * @version $Id$
+ */
+public interface DocumentManager {
+
+    /**
+     * The Avalon component role.
+     */
+    String ROLE = DocumentManager.class.getName();
+
+    /**
+     * Copies a document from one location to another location.
+     * @param sourceDocument The document to copy.
+     * @param destination The destination document.
+     * @throws PublicationException if a document which destinationDocument depends on does not
+     *             exist.
+     */
+    void copy(Document sourceDocument, DocumentLocator destination) throws PublicationException;
+
+    /**
+     * Copies a document to another area.
+     * @param sourceDocument The document to copy.
+     * @param destinationArea The destination area.
+     * @throws PublicationException if a document which the destination document depends on does not
+     *             exist.
+     */
+    void copyToArea(Document sourceDocument, String destinationArea) throws PublicationException;
+
+    /**
+     * Copies a document set to another area.
+     * @param documentSet The document set to copy.
+     * @param destinationArea The destination area.
+     * @throws PublicationException if a document which one of the destination documents depends on
+     *             does not exist.
+     */
+    void copyToArea(DocumentSet documentSet, String destinationArea) throws PublicationException;
+
+    /**
+     * Creates a new document in the same publication the <code>parentDocument</code> belongs to
+     * with the given parameters:
+     * 
+     * @param sourceDocument The document to initialize the contents and meta data from.
+     * @param area The target area.
+     * @param path The target path.
+     * @param language The target language.
+     * @param extension The extension to use for the document source.
+     * @param navigationTitle navigation title
+     * @param visibleInNav determines the visibility of a node in the navigation
+     * @return The added document.
+     * 
+     * @throws DocumentBuildException if the document can not be created
+     * @throws PublicationException if the document is already contained.
+     */
+    Document add(Document sourceDocument, String area, String path, String language,
+            String extension, String navigationTitle, boolean visibleInNav)
+            throws DocumentBuildException, PublicationException;
+
+    /**
+     * Creates a new document with the given parameters:
+     * @param resourceType the document type (aka resource type) of the new document
+     * @param contentSourceUri The URI to read the content from.
+     * @param pub The publication.
+     * @param area The area.
+     * @param path The path.
+     * @param language The language.
+     * @param extension The extension to use for the document source, without the leading dot.
+     * @param navigationTitle The navigation title.
+     * @param visibleInNav The navigation visibility.
+     * @return The added document.
+     * 
+     * @throws DocumentBuildException if the document can not be created
+     * @throws PublicationException if the document is already contained.
+     */
+    Document add(ResourceType resourceType, String contentSourceUri,
+            Publication pub, String area, String path, String language, String extension,
+            String navigationTitle, boolean visibleInNav) throws DocumentBuildException,
+            PublicationException;
+
+    /**
+     * Creates a new document without adding it to the site structure.
+     * @param resourceType the document type (aka resource type) of the new document
+     * @param contentSourceUri The URI to read the content from.
+     * @param pub The publication.
+     * @param area The area.
+     * @param language The language.
+     * @param extension The extension to use for the document source, without the leading dot.
+     * @return The added document.
+     * 
+     * @throws DocumentBuildException if the document can not be created
+     * @throws PublicationException if the document is already contained.
+     */
+    Document add(ResourceType resourceType, String contentSourceUri,
+            Publication pub, String area, String language, String extension)
+            throws DocumentBuildException, PublicationException;
+
+    /**
+     * Adds a new version of a document with a different language and / or in a different area.
+     * 
+     * @param sourceDocument The document to initialize the contents and meta data from.
+     * @param area The area.
+     * @param language The language of the new document.
+     * @return The added document.
+     * 
+     * @throws DocumentBuildException if the document can not be created
+     * @throws PublicationException if the document is already contained.
+     */
+    Document addVersion(Document sourceDocument, String area, String language)
+            throws DocumentBuildException, PublicationException;
+
+    /**
+     * Adds a new version of a document with a different language and / or in a different area.
+     * 
+     * @param sourceDocument The document to initialize the contents and meta data from.
+     * @param area The area.
+     * @param language The language of the new document.
+     * @param addToSite If the new version should be added to the site structure.
+     * @return The added document.
+     * 
+     * @throws DocumentBuildException if the document can not be created
+     * @throws PublicationException if the document is already contained.
+     */
+    Document addVersion(Document sourceDocument, String area, String language, boolean addToSite)
+            throws DocumentBuildException, PublicationException;
+
+    /**
+     * Deletes a document from the content repository and from the site structure.
+     * @param document The document to delete.
+     * @throws PublicationException when something went wrong.
+     */
+    void delete(Document document) throws PublicationException;
+
+    /**
+     * Moves a document from one location to another.
+     * @param sourceDocument The source document.
+     * @param destination The destination document.
+     * @throws PublicationException if a document which the destination document depends on does not
+     *             exist.
+     */
+    void move(Document sourceDocument, DocumentLocator destination) throws PublicationException;
+
+    /**
+     * Moves a document set from one location to another. A source is moved to the destination of
+     * the same position in the set.
+     * @param sources The source documents.
+     * @param destinations The destination documents.
+     * @throws PublicationException if a document which the destination document depends on does not
+     *             exist.
+     */
+    void move(DocumentSet sources, DocumentSet destinations) throws PublicationException;
+
+    /**
+     * Copies a document set from one location to another. A source is copied to the destination of
+     * the same position in the set.
+     * @param sources The source documents.
+     * @param destinations The destination documents.
+     * @throws PublicationException if a document which the destination document depends on does not
+     *             exist.
+     */
+    void copy(DocumentSet sources, DocumentSet destinations) throws PublicationException;
+
+    /**
+     * Moves a document to another location, incl. all requiring documents. If a sitetree is used,
+     * this means that the whole subtree is moved.
+     * @param sourceArea The source area.
+     * @param sourcePath The source path.
+     * @param targetArea The target area.
+     * @param targetPath The target path.
+     * @throws PublicationException if an error occurs.
+     */
+    void moveAll(Area sourceArea, String sourcePath, Area targetArea, String targetPath)
+            throws PublicationException;
+
+    /**
+     * Moves all language versions of a document to another location.
+     * @param sourceArea The source area.
+     * @param sourcePath The source path.
+     * @param targetArea The target area.
+     * @param targetPath The target path.
+     * @throws PublicationException if the documents could not be moved.
+     */
+    void moveAllLanguageVersions(Area sourceArea, String sourcePath, Area targetArea,
+            String targetPath) throws PublicationException;
+
+    /**
+     * Copies a document to another location, incl. all requiring documents. If a sitetree is used,
+     * this means that the whole subtree is copied.
+     * @param sourceArea The source area.
+     * @param sourcePath The source path.
+     * @param targetArea The target area.
+     * @param targetPath The target path.
+     * @throws PublicationException if an error occurs.
+     */
+    void copyAll(Area sourceArea, String sourcePath, Area targetArea, String targetPath)
+            throws PublicationException;
+
+    /**
+     * Copies all language versions of a document to another location.
+     * @param sourceArea The source area.
+     * @param sourcePath The source path.
+     * @param targetArea The target area.
+     * @param targetPath The target path.
+     * @throws PublicationException if the documents could not be copied.
+     */
+    void copyAllLanguageVersions(Area sourceArea, String sourcePath, Area targetArea,
+            String targetPath) throws PublicationException;
+
+    /**
+     * Deletes a document, incl. all requiring documents. If a sitetree is used, this means that the
+     * whole subtree is deleted.
+     * @param document The document.
+     * @throws PublicationException if an error occurs.
+     */
+    void deleteAll(Document document) throws PublicationException;
+
+    /**
+     * Deletes all language versions of a document.
+     * @param document The document.
+     * @throws PublicationException if the documents could not be copied.
+     */
+    void deleteAllLanguageVersions(Document document) throws PublicationException;
+
+    /**
+     * Deletes a set of documents.
+     * @param documents The documents.
+     * @throws PublicationException if an error occurs.
+     */
+    void delete(DocumentSet documents) throws PublicationException;
+
+}
\ No newline at end of file
diff --git a/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentManagerImpl.java b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentManagerImpl.java
new file mode 100644
index 0000000..f49d155
--- /dev/null
+++ b/org.apache.lenya.core.document/src/main/java/org/apache/lenya/cms/publication/DocumentManagerImpl.java
@@ -0,0 +1,862 @@
+/*
+ * 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.publication;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cocoon.spring.configurator.WebAppContextUtils;
+import org.apache.cocoon.util.AbstractLogEnabled;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.apache.excalibur.source.Source;
+import org.apache.excalibur.source.SourceResolver;
+import org.apache.lenya.cms.metadata.MetaData;
+import org.apache.lenya.cms.metadata.MetaDataException;
+import org.apache.lenya.cms.publication.util.DocumentSet;
+import org.apache.lenya.cms.publication.util.DocumentVisitor;
+import org.apache.lenya.cms.repository.Node;
+import org.apache.lenya.cms.repository.NodeFactory;
+import org.apache.lenya.cms.repository.UUIDGenerator;
+import org.apache.lenya.cms.site.Link;
+import org.apache.lenya.cms.site.NodeIterator;
+import org.apache.lenya.cms.site.NodeSet;
+import org.apache.lenya.cms.site.SiteException;
+import org.apache.lenya.cms.site.SiteManager;
+import org.apache.lenya.cms.site.SiteNode;
+import org.apache.lenya.cms.site.SiteStructure;
+import org.apache.lenya.cms.site.SiteUtil;
+
+/**
+ * DocumentManager implementation.
+ * 
+ * @version $Id$
+ */
+public class DocumentManagerImpl extends AbstractLogEnabled implements DocumentManager {
+
+    private SourceResolver sourceResolver;
+    private UUIDGenerator uuidGenerator;
+    private NodeFactory nodeFactory;
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#add(org.apache.lenya.cms.publication.Document,
+     *      java.lang.String, java.lang.String, java.lang.String, java.lang.String,
+     *      java.lang.String, boolean)
+     */
+    public Document add(Document sourceDocument, String area, String path, String language,
+            String extension, String navigationTitle, boolean visibleInNav)
+            throws DocumentBuildException, PublicationException {
+
+        Document document = add(sourceDocument.getResourceType(), sourceDocument.getInputStream(),
+                sourceDocument.getPublication(), area, path, language, extension, navigationTitle,
+                visibleInNav, sourceDocument.getMimeType());
+
+        copyMetaData(sourceDocument, document);
+        return document;
+    }
+
+    /**
+     * Copies meta data from one document to another. If the destination document is a different
+     * area version, the meta data are duplicated (i.e., onCopy = delete is neglected).
+     * @param source
+     * @param destination
+     * @throws PublicationException
+     */
+    protected void copyMetaData(Document source, Document destination) throws PublicationException {
+
+        boolean duplicate = source.getUUID().equals(destination.getUUID())
+                && source.getLanguage().equals(destination.getLanguage())
+                && !source.getArea().equals(destination.getArea());
+
+        try {
+            String[] uris = source.getMetaDataNamespaceUris();
+            for (int i = 0; i < uris.length; i++) {
+                if (duplicate) {
+                    destination.getMetaData(uris[i]).forcedReplaceBy(source.getMetaData(uris[i]));
+                } else {
+                    destination.getMetaData(uris[i]).replaceBy(source.getMetaData(uris[i]));
+                }
+            }
+        } catch (MetaDataException e) {
+            throw new PublicationException(e);
+        }
+    }
+
+    public Document add(ResourceType documentType, String initialContentsURI, Publication pub,
+            String area, String path, String language, String extension, String navigationTitle,
+            boolean visibleInNav) throws DocumentBuildException, DocumentException,
+            PublicationException {
+
+        Area areaObj = pub.getArea(area);
+        SiteStructure site = areaObj.getSite();
+        if (site.contains(path) && site.getNode(path).hasLink(language)) {
+            throw new DocumentException("The link [" + path + ":" + language
+                    + "] is already contained in site [" + site + "]");
+        }
+
+        Document document = add(documentType, initialContentsURI, pub, area, language, extension);
+
+        addToSiteManager(path, document, navigationTitle, visibleInNav);
+        return document;
+    }
+
+    protected Document add(ResourceType documentType, InputStream initialContentsStream,
+            Publication pub, String area, String path, String language, String extension,
+            String navigationTitle, boolean visibleInNav, String mimeType)
+            throws DocumentBuildException, DocumentException, PublicationException {
+
+        Area areaObj = pub.getArea(area);
+        SiteStructure site = areaObj.getSite();
+        if (site.contains(path) && site.getNode(path).hasLink(language)) {
+            throw new DocumentException("The link [" + path + ":" + language
+                    + "] is already contained in site [" + site + "]");
+        }
+
+        Document document = add(documentType, initialContentsStream, pub, area, language,
+                extension, mimeType);
+
+        addToSiteManager(path, document, navigationTitle, visibleInNav);
+        return document;
+    }
+
+    public Document add(ResourceType documentType, String initialContentsURI, Publication pub,
+            String area, String language, String extension) throws DocumentBuildException,
+            DocumentException, PublicationException {
+
+        String uuid = getUuidGenerator().nextUUID();
+        Source source = null;
+        try {
+            source = getSourceResolver().resolveURI(initialContentsURI);
+            return add(documentType, uuid, source.getInputStream(), pub, area, language, extension,
+                    getMimeType(source));
+        } catch (Exception e) {
+            throw new PublicationException(e);
+        } finally {
+            if (source != null) {
+                getSourceResolver().release(source);
+            }
+        }
+    }
+
+    protected String getMimeType(Source source) {
+        String mimeType = source.getMimeType();
+        if (mimeType == null) {
+            mimeType = "";
+        }
+        return mimeType;
+    }
+
+    protected Document add(ResourceType documentType, InputStream initialContentsStream,
+            Publication pub, String area, String language, String extension, String mimeType)
+            throws DocumentBuildException, DocumentException, PublicationException {
+
+        String uuid = getUuidGenerator().nextUUID();
+        return add(documentType, uuid, initialContentsStream, pub, area, language, extension,
+                mimeType);
+    }
+
+    protected Document add(ResourceType documentType, String uuid, InputStream stream,
+            Publication pub, String area, String language, String extension, String mimeType)
+            throws DocumentBuildException {
+        try {
+
+            Area areaObj = pub.getArea(area);
+            if (areaObj.contains(uuid, language)) {
+                throw new DocumentBuildException("The document [" + pub.getId() + ":" + area + ":"
+                        + uuid + ":" + language + "] already exists!");
+            }
+
+            Document document = areaObj.getDocument(uuid, language);
+            document.lock();
+
+            document.setResourceType(documentType);
+            document.setSourceExtension(extension);
+            document.setMimeType(mimeType);
+
+            // Write Lenya-internal meta-data
+            MetaData lenyaMetaData = document.getMetaData(DocumentImpl.METADATA_NAMESPACE);
+            lenyaMetaData.setValue(DocumentImpl.METADATA_CONTENT_TYPE, "xml");
+
+            if (getLogger().isDebugEnabled()) {
+                getLogger().debug("Create");
+                getLogger().debug("    document:     [" + document + "]");
+            }
+
+            create(stream, document);
+            return document;
+        } catch (Exception e) {
+            throw new DocumentBuildException("call to creator for new document failed", e);
+        }
+    }
+
+    protected void create(InputStream stream, Document document) throws Exception {
+
+        // Read initial contents as DOM
+        if (getLogger().isDebugEnabled())
+            getLogger().debug(
+                    "DefaultCreator::create(), ready to read initial contents from URI [" + stream
+                            + "]");
+
+        copy(getSourceResolver(), stream, document);
+    }
+
+    protected void copy(SourceResolver resolver, InputStream sourceInputStream, Document destination)
+            throws IOException {
+
+        boolean useBuffer = true;
+
+        OutputStream destOutputStream = null;
+        try {
+            destOutputStream = destination.getOutputStream();
+
+            if (useBuffer) {
+                final ByteArrayOutputStream sourceBos = new ByteArrayOutputStream();
+                IOUtils.copy(sourceInputStream, sourceBos);
+                IOUtils.write(sourceBos.toByteArray(), destOutputStream);
+            } else {
+                IOUtils.copy(sourceInputStream, destOutputStream);
+            }
+        } finally {
+            if (destOutputStream != null) {
+                destOutputStream.flush();
+                destOutputStream.close();
+            }
+            if (sourceInputStream != null) {
+                sourceInputStream.close();
+            }
+        }
+    }
+
+    protected void addToSiteManager(String path, Document document, String navigationTitle,
+            boolean visibleInNav) throws PublicationException {
+        addToSiteManager(path, document, navigationTitle, visibleInNav, null);
+    }
+
+    protected void addToSiteManager(String path, Document document, String navigationTitle,
+            boolean visibleInNav, String followingSiblingPath) throws PublicationException {
+        SiteStructure site = document.area().getSite();
+        if (!site.contains(path) && followingSiblingPath != null) {
+            site.add(path, followingSiblingPath);
+        }
+        site.add(path, document);
+        document.getLink().setLabel(navigationTitle);
+        document.getLink().getNode().setVisible(visibleInNav);
+    }
+
+    /**
+     * Template method to copy a document. Override {@link #copyDocumentSource(Document, Document)}
+     * to implement access to a custom repository.
+     * @see org.apache.lenya.cms.publication.DocumentManager#copy(org.apache.lenya.cms.publication.Document,
+     *      org.apache.lenya.cms.publication.DocumentLocator)
+     */
+    public void copy(Document sourceDoc, DocumentLocator destination) throws PublicationException {
+
+        if (!destination.getPublicationId().equals(sourceDoc.getPublication().getId())) {
+            throw new PublicationException("Can't copy to a different publication!");
+        }
+
+        SiteStructure destSite = sourceDoc.getPublication().getArea(destination.getArea())
+                .getSite();
+        String destPath = destination.getPath();
+        if (destSite.contains(destination.getPath(), destination.getLanguage())) {
+            Document destDoc = destSite.getNode(destPath).getLink(destination.getLanguage())
+                    .getDocument();
+            copyDocumentSource(sourceDoc, destDoc);
+            copyInSiteStructure(sourceDoc, destDoc, destPath);
+        } else {
+            add(sourceDoc, destination.getArea(), destPath, destination.getLanguage(), sourceDoc
+                    .getExtension(), sourceDoc.getLink().getLabel(), sourceDoc.getLink().getNode()
+                    .isVisible());
+        }
+
+    }
+
+    protected void copyInSiteStructure(Document sourceDoc, Document destDoc, String destPath)
+            throws PublicationException, DocumentException, SiteException {
+
+        String destArea = destDoc.getArea();
+
+        SiteStructure destSite = sourceDoc.getPublication().getArea(destArea).getSite();
+
+        if (sourceDoc.hasLink()) {
+            if (destDoc.hasLink()) {
+                Link srcLink = sourceDoc.getLink();
+                Link destLink = destDoc.getLink();
+                destLink.setLabel(srcLink.getLabel());
+                destLink.getNode().setVisible(srcLink.getNode().isVisible());
+            } else {
+                String label = sourceDoc.getLink().getLabel();
+                boolean visible = sourceDoc.getLink().getNode().isVisible();
+                if (destSite.contains(sourceDoc.getLink().getNode().getPath())) {
+                    addToSiteManager(destPath, destDoc, label, visible);
+                } else {
+
+                    String followingSiblingPath = null;
+
+                    if (sourceDoc.getPath().equals(destPath)) {
+                        SiteStructure sourceSite = sourceDoc.area().getSite();
+
+                        SiteNode[] sourceSiblings;
+                        SiteNode sourceNode = sourceDoc.getLink().getNode();
+                        if (sourceNode.isTopLevel()) {
+                            sourceSiblings = sourceSite.getTopLevelNodes();
+                        } else if (sourceNode.getParent() != null) {
+                            sourceSiblings = sourceNode.getParent().getChildren();
+                        } else {
+                            sourceSiblings = new SiteNode[1];
+                            sourceSiblings[0] = sourceNode;
+                        }
+
+                        final int sourcePos = Arrays.asList(sourceSiblings).indexOf(sourceNode);
+
+                        int pos = sourcePos;
+                        while (followingSiblingPath == null && pos < sourceSiblings.length) {
+                            String siblingPath = sourceSiblings[pos].getPath();
+                            if (destSite.contains(siblingPath)) {
+                                followingSiblingPath = siblingPath;
+                            }
+                            pos++;
+                        }
+                    }
+
+                    if (followingSiblingPath == null) {
+                        addToSiteManager(destPath, destDoc, label, visible);
+                    } else {
+                        addToSiteManager(destPath, destDoc, label, visible, followingSiblingPath);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#delete(org.apache.lenya.cms.publication.Document)
+     */
+    public void delete(Document document) throws PublicationException {
+        if (!document.exists()) {
+            throw new PublicationException("Document [" + document + "] does not exist!");
+        }
+
+        if (document.hasLink()) {
+            document.getLink().delete();
+        }
+
+        document.delete();
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#move(org.apache.lenya.cms.publication.Document,
+     *      org.apache.lenya.cms.publication.DocumentLocator)
+     */
+    public void move(Document sourceDocument, DocumentLocator destination)
+            throws PublicationException {
+
+        if (!destination.getArea().equals(sourceDocument.getArea())) {
+            throw new PublicationException("Can't move to a different area!");
+        }
+
+        SiteStructure site = sourceDocument.area().getSite();
+        if (site.contains(destination.getPath())) {
+            throw new PublicationException("The path [" + destination
+                    + "] is already contained in this publication!");
+        }
+
+        String label = sourceDocument.getLink().getLabel();
+        boolean visible = sourceDocument.getLink().getNode().isVisible();
+        sourceDocument.getLink().delete();
+
+        site.add(destination.getPath(), sourceDocument);
+        sourceDocument.getLink().setLabel(label);
+        sourceDocument.getLink().getNode().setVisible(visible);
+
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#copyToArea(org.apache.lenya.cms.publication.Document,
+     *      java.lang.String)
+     */
+    public void copyToArea(Document sourceDoc, String destinationArea) throws PublicationException {
+        String language = sourceDoc.getLanguage();
+        copyToVersion(sourceDoc, destinationArea, language);
+    }
+
+    protected void copyToVersion(Document sourceDoc, String destinationArea, String language)
+            throws DocumentException, DocumentBuildException, PublicationException, SiteException {
+
+        Document destDoc;
+        if (sourceDoc.existsAreaVersion(destinationArea)) {
+            destDoc = sourceDoc.getAreaVersion(destinationArea);
+            copyDocumentSource(sourceDoc, destDoc);
+        } else {
+            destDoc = addVersion(sourceDoc, destinationArea, language);
+        }
+
+        if (sourceDoc.hasLink()) {
+            copyInSiteStructure(sourceDoc, destDoc, sourceDoc.getPath());
+        }
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#copyToArea(org.apache.lenya.cms.publication.util.DocumentSet,
+     *      java.lang.String)
+     */
+    public void copyToArea(DocumentSet documentSet, String destinationArea)
+            throws PublicationException {
+        Document[] documents = documentSet.getDocuments();
+        for (int i = 0; i < documents.length; i++) {
+            copyToArea(documents[i], destinationArea);
+        }
+    }
+
+    public void moveAll(Area sourceArea, String sourcePath, Area targetArea, String targetPath)
+            throws PublicationException {
+        SiteStructure site = sourceArea.getSite();
+
+        SiteNode root = site.getNode(sourcePath);
+        List subsite = preOrder(root);
+
+        for (Iterator n = subsite.iterator(); n.hasNext();) {
+            SiteNode node = (SiteNode) n.next();
+            String subPath = node.getPath().substring(sourcePath.length());
+            targetArea.getSite().add(targetPath + subPath);
+        }
+        Collections.reverse(subsite);
+        for (Iterator n = subsite.iterator(); n.hasNext();) {
+            SiteNode node = (SiteNode) n.next();
+            String subPath = node.getPath().substring(sourcePath.length());
+            moveAllLanguageVersions(sourceArea, sourcePath + subPath, targetArea, targetPath
+                    + subPath);
+        }
+    }
+
+    protected List preOrder(SiteNode node) {
+        List list = new ArrayList();
+        list.add(node);
+        SiteNode[] children = node.getChildren();
+        for (int i = 0; i < children.length; i++) {
+            list.addAll(preOrder(children[i]));
+        }
+        return list;
+    }
+
+    public void moveAllLanguageVersions(Area sourceArea, String sourcePath, Area targetArea,
+            String targetPath) throws PublicationException {
+
+        SiteNode sourceNode = sourceArea.getSite().getNode(sourcePath);
+        String[] languages = sourceNode.getLanguages();
+        for (int i = 0; i < languages.length; i++) {
+            Link sourceLink = sourceNode.getLink(languages[i]);
+            String label = sourceLink.getLabel();
+            Document sourceDoc = sourceLink.getDocument();
+            sourceLink.delete();
+
+            Document targetDoc;
+            if (sourceArea.getName().equals(targetArea.getName())) {
+                targetDoc = sourceDoc;
+            } else {
+                targetDoc = addVersion(sourceDoc, targetArea.getName(), sourceDoc.getLanguage());
+                copyRevisions(sourceDoc, targetDoc);
+                sourceDoc.delete();
+            }
+
+            Link link = targetArea.getSite().add(targetPath, targetDoc);
+            link.setLabel(label);
+            assert targetDoc.getLink().getLabel().equals(label);
+        }
+        SiteNode targetNode = targetArea.getSite().getNode(targetPath);
+        targetNode.setVisible(sourceNode.isVisible());
+    }
+
+    protected void copyRevisions(Document sourceDoc, Document targetDoc)
+            throws PublicationException {
+        try {
+            Node targetNode = ((DocumentImpl) targetDoc).getRepositoryNode();
+            targetNode.copyRevisionsFrom(((DocumentImpl) sourceDoc).getRepositoryNode());
+        } catch (Exception e) {
+            throw new PublicationException(e);
+        }
+    }
+
+    public void copyAll(Area sourceArea, String sourcePath, Area targetArea, String targetPath)
+            throws PublicationException {
+
+        SiteStructure site = sourceArea.getSite();
+        SiteNode root = site.getNode(sourcePath);
+
+        List preOrder = preOrder(root);
+        for (Iterator i = preOrder.iterator(); i.hasNext();) {
+            SiteNode node = (SiteNode) i.next();
+            String nodeSourcePath = node.getPath();
+            String nodeTargetPath = targetPath + nodeSourcePath.substring(sourcePath.length());
+            copyAllLanguageVersions(sourceArea, nodeSourcePath, targetArea, nodeTargetPath);
+        }
+    }
+
+    public void copyAllLanguageVersions(Area sourceArea, String sourcePath, Area targetArea,
+            String targetPath) throws PublicationException {
+        Publication pub = sourceArea.getPublication();
+
+        SiteNode sourceNode = sourceArea.getSite().getNode(sourcePath);
+        String[] languages = sourceNode.getLanguages();
+
+        Document targetDoc = null;
+
+        for (int i = 0; i < languages.length; i++) {
+            Document sourceVersion = sourceNode.getLink(languages[i]).getDocument();
+            DocumentLocator targetLocator = DocumentLocator.getLocator(pub.getId(), targetArea
+                    .getName(), targetPath, languages[i]);
+            if (targetDoc == null) {
+                copy(sourceVersion, targetLocator.getLanguageVersion(languages[i]));
+                targetDoc = targetArea.getSite().getNode(targetPath).getLink(languages[i])
+                        .getDocument();
+            } else {
+                targetDoc = addVersion(targetDoc, targetLocator.getArea(), languages[i]);
+                addToSiteManager(targetLocator.getPath(), targetDoc, sourceVersion.getLink()
+                        .getLabel(), sourceVersion.getLink().getNode().isVisible());
+                copyDocumentSource(sourceVersion, targetDoc);
+            }
+        }
+    }
+
+    /**
+     * Copies a document source.
+     * @param sourceDocument The source document.
+     * @param destinationDocument The destination document.
+     * @throws PublicationException when something went wrong.
+     */
+    public void copyDocumentSource(Document sourceDocument, Document destinationDocument)
+            throws PublicationException {
+        copyContent(sourceDocument, destinationDocument);
+        copyMetaData(sourceDocument, destinationDocument);
+    }
+
+    protected void copyContent(Document sourceDocument, Document destinationDocument)
+            throws PublicationException {
+        boolean useBuffer = true;
+
+        OutputStream destOutputStream = null;
+        InputStream sourceInputStream = null;
+        try {
+            try {
+                sourceInputStream = sourceDocument.getInputStream();
+                destOutputStream = destinationDocument.getOutputStream();
+
+                if (useBuffer) {
+                    final ByteArrayOutputStream sourceBos = new ByteArrayOutputStream();
+                    IOUtils.copy(sourceInputStream, sourceBos);
+                    IOUtils.write(sourceBos.toByteArray(), destOutputStream);
+                } else {
+                    IOUtils.copy(sourceInputStream, destOutputStream);
+                }
+            } finally {
+                if (destOutputStream != null) {
+                    destOutputStream.flush();
+                    destOutputStream.close();
+                }
+                if (sourceInputStream != null) {
+                    sourceInputStream.close();
+                }
+            }
+        } catch (Exception e) {
+            throw new PublicationException(e);
+        }
+    }
+
+    /**
+     * Abstract base class for document visitors which operate on a source and target document.
+     */
+    public static abstract class SourceTargetVisitor implements DocumentVisitor {
+
+        private DocumentLocator rootSource;
+        private DocumentLocator rootTarget;
+        private DocumentManager manager;
+
+        /**
+         * Ctor.
+         * @param manager The document manager.
+         * @param source The root source.
+         * @param target The root target.
+         */
+        public SourceTargetVisitor(DocumentManager manager, Document source, DocumentLocator target) {
+            this.manager = manager;
+            this.rootSource = source.getLocator();
+            this.rootTarget = target;
+        }
+
+        /**
+         * @return the root source
+         */
+        protected DocumentLocator getRootSource() {
+            return rootSource;
+        }
+
+        /**
+         * @return the root target
+         */
+        protected DocumentLocator getRootTarget() {
+            return rootTarget;
+        }
+
+        /**
+         * @return the document manager
+         */
+        protected DocumentManager getDocumentManager() {
+            return this.manager;
+        }
+
+        /**
+         * Returns the target corresponding to a source relatively to the root target document.
+         * @param source The source.
+         * @return A document.
+         * @throws DocumentBuildException if the target could not be built.
+         */
+        protected DocumentLocator getTarget(Document source) throws DocumentBuildException {
+            DocumentLocator sourceLocator = source.getLocator();
+            String rootSourcePath = getRootSource().getPath();
+            if (sourceLocator.getPath().equals(rootSourcePath)) {
+                return rootTarget;
+            } else {
+                String relativePath = sourceLocator.getPath().substring(rootSourcePath.length());
+                return rootTarget.getDescendant(relativePath);
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#deleteAll(org.apache.lenya.cms.publication.Document)
+     */
+    public void deleteAll(Document document) throws PublicationException {
+        NodeSet subsite = SiteUtil.getSubSite(document.getLink().getNode());
+        for (NodeIterator i = subsite.descending(); i.hasNext();) {
+            SiteNode node = i.next();
+            String[] languages = node.getLanguages();
+            for (int l = 0; l < languages.length; l++) {
+                Document doc = node.getLink(languages[l]).getDocument();
+                delete(doc);
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#deleteAllLanguageVersions(org.apache.lenya.cms.publication.Document)
+     */
+    public void deleteAllLanguageVersions(Document document) throws PublicationException {
+        String[] languages = document.getLanguages();
+        for (int i = 0; i < languages.length; i++) {
+            delete(document.getTranslation(languages[i]));
+        }
+    }
+
+    /**
+     * Visitor to delete documents.
+     */
+    public static class DeleteVisitor implements DocumentVisitor {
+
+        private DocumentManager manager;
+
+        /**
+         * Ctor.
+         * @param manager The document manager.
+         */
+        public DeleteVisitor(DocumentManager manager) {
+            this.manager = manager;
+        }
+
+        protected DocumentManager getDocumentManager() {
+            return this.manager;
+        }
+
+        /**
+         * @see org.apache.lenya.cms.publication.util.DocumentVisitor#visitDocument(org.apache.lenya.cms.publication.Document)
+         */
+        public void visitDocument(Document document) throws PublicationException {
+            getDocumentManager().deleteAllLanguageVersions(document);
+        }
+
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#delete(org.apache.lenya.cms.publication.util.DocumentSet)
+     */
+    public void delete(DocumentSet documents) throws PublicationException {
+
+        if (documents.isEmpty()) {
+            return;
+        }
+
+        DocumentSet set = new DocumentSet(documents.getDocuments());
+        sortAscending(set);
+        set.reverse();
+
+        DocumentVisitor visitor = new DeleteVisitor(this);
+        try {
+            set.visit(visitor);
+        } catch (Exception e) {
+            throw new PublicationException(e);
+        }
+
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#move(org.apache.lenya.cms.publication.util.DocumentSet,
+     *      org.apache.lenya.cms.publication.util.DocumentSet)
+     */
+    public void move(DocumentSet sources, DocumentSet destinations) throws PublicationException {
+        copy(sources, destinations);
+        delete(sources);
+        /*
+         * Document[] sourceDocs = sources.getDocuments(); Document[] targetDocs =
+         * destinations.getDocuments();
+         * 
+         * if (sourceDocs.length != targetDocs.length) { throw new PublicationException( "The number
+         * of source and destination documents must be equal!"); }
+         * 
+         * Map source2target = new HashMap(); for (int i = 0; i < sourceDocs.length; i++) {
+         * source2target.put(sourceDocs[i], targetDocs[i]); }
+         * 
+         * DocumentSet sortedSources = new DocumentSet(sourceDocs);
+         * SiteUtil.sortAscending(this.manager, sortedSources); Document[] sortedSourceDocs =
+         * sortedSources.getDocuments();
+         * 
+         * for (int i = 0; i < sortedSourceDocs.length; i++) { move(sortedSourceDocs[i], (Document)
+         * source2target.get(sortedSourceDocs[i])); }
+         */
+    }
+
+    /**
+     * @see org.apache.lenya.cms.publication.DocumentManager#copy(org.apache.lenya.cms.publication.util.DocumentSet,
+     *      org.apache.lenya.cms.publication.util.DocumentSet)
+     */
+    public void copy(DocumentSet sources, DocumentSet destinations) throws PublicationException {
+        Document[] sourceDocs = sources.getDocuments();
+        Document[] targetDocs = destinations.getDocuments();
+
+        if (sourceDocs.length != targetDocs.length) {
+            throw new PublicationException(
+                    "The number of source and destination documents must be equal!");
+        }
+
+        Map source2target = new HashMap();
+        for (int i = 0; i < sourceDocs.length; i++) {
+            source2target.put(sourceDocs[i], targetDocs[i]);
+        }
+
+        DocumentSet sortedSources = new DocumentSet(sourceDocs);
+        sortAscending(sortedSources);
+        Document[] sortedSourceDocs = sortedSources.getDocuments();
+
+        for (int i = 0; i < sortedSourceDocs.length; i++) {
+            copy(sortedSourceDocs[i], ((Document) source2target.get(sortedSourceDocs[i]))
+                    .getLocator());
+        }
+    }
+
+    protected void sortAscending(DocumentSet set) throws PublicationException {
+
+        if (!set.isEmpty()) {
+
+            Document[] docs = set.getDocuments();
+            int n = docs.length;
+
+            Publication pub = docs[0].getPublication();
+            SiteManager siteManager = (SiteManager) WebAppContextUtils
+                    .getCurrentWebApplicationContext().getBean(
+                            SiteManager.class.getName() + "/" + pub.getSiteManagerHint());
+
+            Set nodes = new HashSet();
+            for (int i = 0; i < docs.length; i++) {
+                nodes.add(docs[i].getLink().getNode());
+            }
+
+            SiteNode[] ascending = siteManager.sortAscending((SiteNode[]) nodes
+                    .toArray(new SiteNode[nodes.size()]));
+
+            set.clear();
+            for (int i = 0; i < ascending.length; i++) {
+                for (int d = 0; d < docs.length; d++) {
+                    if (docs[d].getPath().equals(ascending[i].getPath())) {
+                        set.add(docs[d]);
+                    }
+                }
+            }
+
+            if (set.getDocuments().length != n) {
+                throw new IllegalStateException("Number of documents has changed!");
+            }
+
+        }
+    }
+
+    public Document addVersion(Document sourceDocument, String area, String language,
+            boolean addToSiteStructure) throws DocumentBuildException, PublicationException {
+        Document document = addVersion(sourceDocument, area, language);
+
+        if (addToSiteStructure && sourceDocument.hasLink()) {
+            String path = sourceDocument.getPath();
+            boolean visible = sourceDocument.getLink().getNode().isVisible();
+            addToSiteManager(path, document, sourceDocument.getLink().getLabel(), visible);
+        }
+
+        return document;
+    }
+
+    public Document addVersion(Document sourceDocument, String area, String language)
+            throws DocumentBuildException, DocumentException, PublicationException {
+        Document document = add(sourceDocument.getResourceType(), sourceDocument.getUUID(),
+                sourceDocument.getInputStream(), sourceDocument.getPublication(), area, language,
+                sourceDocument.getSourceExtension(), sourceDocument.getMimeType());
+        copyMetaData(sourceDocument, document);
+
+        return document;
+    }
+
+    public SourceResolver getSourceResolver() {
+        return sourceResolver;
+    }
+
+    public void setSourceResolver(SourceResolver sourceResolver) {
+        this.sourceResolver = sourceResolver;
+    }
+
+    public UUIDGenerator getUuidGenerator() {
+        return uuidGenerator;
+    }
+
+    public void setUuidGenerator(UUIDGenerator uuidGenerator) {
+        this.uuidGenerator = uuidGenerator;
+    }
+
+    public NodeFactory getNodeFactory() {
+        return nodeFactory;
+    }
+
+    public void setNodeFactory(NodeFactory nodeFactory) {
+        this.nodeFactory = nodeFactory;
+    }
+
+}