Clarify ACR API
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / acr / xml / DomContent.java
index 9e370db0c07ebfd552186300c59b677ff87952c3..1d751149101caaabad4e17b697b4bb699de94e9e 100644 (file)
@@ -1,30 +1,53 @@
 package org.argeo.cms.acr.xml;
 
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.CharBuffer;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
 
 import javax.xml.XMLConstants;
 import javax.xml.namespace.NamespaceContext;
 import javax.xml.namespace.QName;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentName;
-import org.argeo.api.acr.spi.AbstractContent;
+import org.argeo.api.acr.CrAttributeType;
+import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.cms.acr.AbstractContent;
+import org.argeo.cms.acr.CmsContent;
 import org.w3c.dom.Attr;
+import org.w3c.dom.DOMException;
 import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
 import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 import org.w3c.dom.Text;
 
+/** Content persisted as a DOM element. */
 public class DomContent extends AbstractContent implements ProvidedContent {
 
-       private final ProvidedSession session;
        private final DomContentProvider provider;
        private final Element element;
 
@@ -32,7 +55,7 @@ public class DomContent extends AbstractContent implements ProvidedContent {
        private Boolean hasText = null;
 
        public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) {
-               this.session = session;
+               super(session);
                this.provider = contentProvider;
                this.element = element;
        }
@@ -43,9 +66,24 @@ public class DomContent extends AbstractContent implements ProvidedContent {
 
        @Override
        public QName getName() {
+               if (isLocalRoot()) {// root
+                       String mountPath = provider.getMountPath();
+                       if (mountPath != null) {
+                               if (Content.ROOT_PATH.equals(mountPath)) {
+                                       return CrName.root.qName();
+                               }
+                               Content mountPoint = getSession().getMountPoint(mountPath);
+                               QName mountPointName = mountPoint.getName();
+                               return mountPointName;
+                       }
+               }
                return toQName(this.element);
        }
 
+       protected boolean isLocalRoot() {
+               return element.getParentNode() == null || element.getParentNode() instanceof Document;
+       }
+
        protected QName toQName(Node node) {
                String prefix = node.getPrefix();
                if (prefix == null) {
@@ -93,24 +131,25 @@ public class DomContent extends AbstractContent implements ProvidedContent {
                for (int i = 0; i < attributes.getLength(); i++) {
                        Attr attr = (Attr) attributes.item(i);
                        QName key = toQName(attr);
+                       if (key.getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI))
+                               continue;// skip prefix mapping
                        result.add(key);
                }
                return result;
        }
 
-       @SuppressWarnings("unchecked")
+//     @SuppressWarnings("unchecked")
        @Override
        public <A> Optional<A> get(QName key, Class<A> clss) {
                String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null
                                : key.getNamespaceURI();
                if (element.hasAttributeNS(namespaceUriOrNull, key.getLocalPart())) {
                        String value = element.getAttributeNS(namespaceUriOrNull, key.getLocalPart());
-                       if (clss.isAssignableFrom(String.class))
-                               return Optional.of((A) value);
-                       else
-                               return Optional.empty();
+//                     if (isDefaultAttrTypeRequested(clss))
+//                             return Optional.of((A) CrAttributeType.parse(value));
+                       return CrAttributeType.cast(clss, value);
                } else
-                       return null;
+                       return Optional.empty();
        }
 
        @Override
@@ -118,12 +157,31 @@ public class DomContent extends AbstractContent implements ProvidedContent {
                Object previous = get(key);
                String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null
                                : key.getNamespaceURI();
+               String prefixToUse = registerPrefixIfNeeded(key);
                element.setAttributeNS(namespaceUriOrNull,
-                               namespaceUriOrNull == null ? key.getLocalPart() : key.getPrefix() + ":" + key.getLocalPart(),
+                               namespaceUriOrNull == null ? key.getLocalPart() : prefixToUse + ":" + key.getLocalPart(),
                                value.toString());
                return previous;
        }
 
+       protected String registerPrefixIfNeeded(QName name) {
+               String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
+                               : name.getNamespaceURI();
+               String prefixToUse;
+               if (namespaceUriOrNull != null) {
+                       String registeredPrefix = provider.getPrefix(namespaceUriOrNull);
+                       if (registeredPrefix != null) {
+                               prefixToUse = registeredPrefix;
+                       } else {
+                               provider.registerPrefix(name.getPrefix(), namespaceUriOrNull);
+                               prefixToUse = name.getPrefix();
+                       }
+               } else {
+                       prefixToUse = null;
+               }
+               return prefixToUse;
+       }
+
        @Override
        public boolean hasText() {
 //             return element instanceof Text;
@@ -168,19 +226,27 @@ public class DomContent extends AbstractContent implements ProvidedContent {
        @Override
        public Iterator<Content> iterator() {
                NodeList nodeList = element.getChildNodes();
-               return new ElementIterator(this, session, provider, nodeList);
+               return new ElementIterator(this, getSession(), provider, nodeList);
        }
 
        @Override
        public Content getParent() {
-               Node parent = element.getParentNode();
-               if (parent == null)
-                       return null;
-               if (parent instanceof Document)
-                       return null;
-               if (!(parent instanceof Element))
+               Node parentNode = element.getParentNode();
+               if (isLocalRoot()) {
+                       String mountPath = provider.getMountPath();
+                       if (mountPath == null)
+                               return null;
+                       if (Content.ROOT_PATH.equals(mountPath)) {
+                               return null;
+                       }
+                       String[] parent = CmsContent.getParentPath(mountPath);
+                       if ("".equals(parent[0]))
+                               return null;
+                       return getSession().get(parent[0]);
+               }
+               if (!(parentNode instanceof Element))
                        throw new IllegalStateException("Parent is not an element");
-               return new DomContent(this, (Element) parent);
+               return new DomContent(this, (Element) parentNode);
        }
 
        @Override
@@ -189,8 +255,9 @@ public class DomContent extends AbstractContent implements ProvidedContent {
                Document document = this.element.getOwnerDocument();
                String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
                                : name.getNamespaceURI();
+               String prefixToUse = registerPrefixIfNeeded(name);
                Element child = document.createElementNS(namespaceUriOrNull,
-                               namespaceUriOrNull == null ? name.getLocalPart() : name.getPrefix() + ":" + name.getLocalPart());
+                               namespaceUriOrNull == null ? name.getLocalPart() : prefixToUse + ":" + name.getLocalPart());
                element.appendChild(child);
                return new DomContent(this, child);
        }
@@ -211,10 +278,153 @@ public class DomContent extends AbstractContent implements ProvidedContent {
 
        }
 
-       public ProvidedSession getSession() {
-               return session;
+       @SuppressWarnings("unchecked")
+       @Override
+       public <A> A adapt(Class<A> clss) throws IllegalArgumentException {
+               if (CharBuffer.class.isAssignableFrom(clss)) {
+                       String textContent = element.getTextContent();
+                       CharBuffer buf = CharBuffer.wrap(textContent);
+                       return (A) buf;
+               } else if (Source.class.isAssignableFrom(clss)) {
+                       DOMSource source = new DOMSource(element);
+                       return (A) source;
+               }
+               return super.adapt(clss);
+       }
+
+       @SuppressWarnings("unchecked")
+       public <A> CompletableFuture<A> write(Class<A> clss) {
+               if (String.class.isAssignableFrom(clss)) {
+                       CompletableFuture<String> res = new CompletableFuture<>();
+                       res.thenAccept((s) -> {
+                               getSession().notifyModification(this);
+                               element.setTextContent(s);
+                       });
+                       return (CompletableFuture<A>) res;
+               } else if (Source.class.isAssignableFrom(clss)) {
+                       CompletableFuture<Source> res = new CompletableFuture<>();
+                       res.thenAccept((source) -> {
+                               try {
+                                       Transformer transformer = provider.getTransformerFactory().newTransformer();
+                                       DocumentFragment documentFragment = element.getOwnerDocument().createDocumentFragment();
+                                       DOMResult result = new DOMResult(documentFragment);
+                                       transformer.transform(source, result);
+                                       // Node parentNode = element.getParentNode();
+                                       Element resultElement = (Element) documentFragment.getFirstChild();
+                                       QName resultName = toQName(resultElement);
+                                       if (!resultName.equals(getName()))
+                                               throw new IllegalArgumentException(resultName + "+ is not compatible with " + getName());
+
+                                       // attributes
+                                       NamedNodeMap attrs = resultElement.getAttributes();
+                                       for (int i = 0; i < attrs.getLength(); i++) {
+                                               Attr attr2 = (Attr) element.getOwnerDocument().importNode(attrs.item(i), true);
+                                               element.getAttributes().setNamedItem(attr2);
+                                       }
+
+                                       // Move all the children
+                                       while (element.hasChildNodes()) {
+                                               element.removeChild(element.getFirstChild());
+                                       }
+                                       while (resultElement.hasChildNodes()) {
+                                               element.appendChild(resultElement.getFirstChild());
+                                       }
+//                                     parentNode.replaceChild(resultNode, element);
+//                                     element = (Element)resultNode;
+
+                               } catch (DOMException | TransformerException e) {
+                                       throw new RuntimeException("Cannot write to element", e);
+                               }
+                       });
+                       return (CompletableFuture<A>) res;
+               }
+               return super.write(clss);
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
+               if (InputStream.class.isAssignableFrom(clss)) {
+                       PipedOutputStream out = new PipedOutputStream();
+                       ForkJoinPool.commonPool().execute(() -> {
+                               try {
+                                       Source source = new DOMSource(element);
+                                       Result result = new StreamResult(out);
+                                       provider.getTransformerFactory().newTransformer().transform(source, result);
+                                       out.flush();
+                                       out.close();
+                               } catch (TransformerException | IOException e) {
+                                       throw new RuntimeException("Cannot read " + getPath(), e);
+                               }
+                       });
+                       return (C) new PipedInputStream(out);
+               }
+               return super.open(clss);
+       }
+
+       @Override
+       public int getSiblingIndex() {
+               Node curr = element.getPreviousSibling();
+               int count = 1;
+               while (curr != null) {
+                       if (curr instanceof Element) {
+                               if (Objects.equals(curr.getNamespaceURI(), element.getNamespaceURI())
+                                               && Objects.equals(curr.getLocalName(), element.getLocalName())) {
+                                       count++;
+                               }
+                       }
+                       curr = curr.getPreviousSibling();
+               }
+               return count;
+       }
+
+       /*
+        * TYPING
+        */
+       @Override
+       public List<QName> getContentClasses() {
+               List<QName> res = new ArrayList<>();
+               if (isLocalRoot()) {
+                       String mountPath = provider.getMountPath();
+                       if (Content.ROOT_PATH.equals(mountPath)) {// repository root
+                               res.add(CrName.root.qName());
+                       } else {
+                               Content mountPoint = getSession().getMountPoint(mountPath);
+                               res.addAll(mountPoint.getContentClasses());
+                       }
+               } else {
+                       res.add(getName());
+               }
+               return res;
+       }
+
+       @Override
+       public void addContentClasses(QName... contentClass) {
+               if (isLocalRoot()) {
+                       String mountPath = provider.getMountPath();
+                       if (Content.ROOT_PATH.equals(mountPath)) {// repository root
+                               throw new IllegalArgumentException("Cannot add content classes to repository root");
+                       } else {
+                               Content mountPoint = getSession().getMountPoint(mountPath);
+                               mountPoint.addContentClasses(contentClass);
+                       }
+               } else {
+                       super.addContentClasses(contentClass);
+               }
+       }
+
+       /*
+        * MOUNT MANAGEMENT
+        */
+       @Override
+       public ProvidedContent getMountPoint(String relativePath) {
+               // FIXME use qualified names
+               Element childElement = (Element) element.getElementsByTagName(relativePath).item(0);
+               // TODO check that it is a mount
+               return new DomContent(this, childElement);
        }
 
+       @Override
        public DomContentProvider getProvider() {
                return provider;
        }