1 package org
.argeo
.cms
.acr
.xml
;
3 import java
.io
.Closeable
;
4 import java
.io
.IOException
;
5 import java
.io
.InputStream
;
6 import java
.io
.PipedInputStream
;
7 import java
.io
.PipedOutputStream
;
8 import java
.nio
.CharBuffer
;
9 import java
.util
.ArrayList
;
10 import java
.util
.HashSet
;
11 import java
.util
.Iterator
;
12 import java
.util
.List
;
13 import java
.util
.Objects
;
14 import java
.util
.Optional
;
16 import java
.util
.concurrent
.CompletableFuture
;
17 import java
.util
.concurrent
.ForkJoinPool
;
19 import javax
.xml
.XMLConstants
;
20 import javax
.xml
.namespace
.NamespaceContext
;
21 import javax
.xml
.namespace
.QName
;
22 import javax
.xml
.transform
.Result
;
23 import javax
.xml
.transform
.Source
;
24 import javax
.xml
.transform
.Transformer
;
25 import javax
.xml
.transform
.TransformerException
;
26 import javax
.xml
.transform
.dom
.DOMResult
;
27 import javax
.xml
.transform
.dom
.DOMSource
;
28 import javax
.xml
.transform
.stream
.StreamResult
;
30 import org
.argeo
.api
.acr
.Content
;
31 import org
.argeo
.api
.acr
.ContentName
;
32 import org
.argeo
.api
.acr
.CrAttributeType
;
33 import org
.argeo
.api
.acr
.CrName
;
34 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
35 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
36 import org
.argeo
.cms
.acr
.AbstractContent
;
37 import org
.argeo
.cms
.acr
.ContentUtils
;
38 import org
.w3c
.dom
.Attr
;
39 import org
.w3c
.dom
.DOMException
;
40 import org
.w3c
.dom
.Document
;
41 import org
.w3c
.dom
.DocumentFragment
;
42 import org
.w3c
.dom
.Element
;
43 import org
.w3c
.dom
.NamedNodeMap
;
44 import org
.w3c
.dom
.Node
;
45 import org
.w3c
.dom
.NodeList
;
46 import org
.w3c
.dom
.Text
;
48 /** Content persisted as a DOM element. */
49 public class DomContent
extends AbstractContent
implements ProvidedContent
{
51 private final DomContentProvider provider
;
52 private final Element element
;
54 // private String text = null;
55 private Boolean hasText
= null;
57 public DomContent(ProvidedSession session
, DomContentProvider contentProvider
, Element element
) {
59 this.provider
= contentProvider
;
60 this.element
= element
;
63 public DomContent(DomContent context
, Element element
) {
64 this(context
.getSession(), context
.getProvider(), element
);
68 public QName
getName() {
69 if (isLocalRoot()) {// root
70 String mountPath
= provider
.getMountPath();
71 if (mountPath
!= null) {
72 if (Content
.ROOT_PATH
.equals(mountPath
)) {
73 return CrName
.root
.qName();
75 Content mountPoint
= getSession().getMountPoint(mountPath
);
76 QName mountPointName
= mountPoint
.getName();
77 return mountPointName
;
80 return toQName(this.element
);
83 protected boolean isLocalRoot() {
84 return element
.getParentNode() == null || element
.getParentNode() instanceof Document
;
87 protected QName
toQName(Node node
) {
88 String prefix
= node
.getPrefix();
90 String namespaceURI
= node
.getNamespaceURI();
91 if (namespaceURI
== null)
92 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(null);
93 if (namespaceURI
== null) {
94 return toQName(node
, node
.getLocalName());
96 String contextPrefix
= provider
.getPrefix(namespaceURI
);
97 if (contextPrefix
== null)
98 throw new IllegalStateException("Namespace " + namespaceURI
+ " is unbound");
99 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
102 String namespaceURI
= node
.getNamespaceURI();
103 if (namespaceURI
== null)
104 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(prefix
);
105 if (namespaceURI
== null) {
106 namespaceURI
= provider
.getNamespaceURI(prefix
);
107 if (XMLConstants
.NULL_NS_URI
.equals(namespaceURI
))
108 throw new IllegalStateException("Prefix " + prefix
+ " is unbound");
109 // TODO bind the prefix in the document?
111 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
115 protected QName
toQName(Node source
, String namespaceURI
, String localName
, NamespaceContext namespaceContext
) {
116 return new ContentName(namespaceURI
, localName
, namespaceContext
);
119 protected QName
toQName(Node source
, String localName
) {
120 return new ContentName(localName
);
123 * ATTRIBUTES OPERATIONS
127 public Iterable
<QName
> keys() {
128 // TODO implement an iterator?
129 Set
<QName
> result
= new HashSet
<>();
130 NamedNodeMap attributes
= element
.getAttributes();
131 for (int i
= 0; i
< attributes
.getLength(); i
++) {
132 Attr attr
= (Attr
) attributes
.item(i
);
133 QName key
= toQName(attr
);
134 if (key
.getNamespaceURI().equals(XMLConstants
.XMLNS_ATTRIBUTE_NS_URI
))
135 continue;// skip prefix mapping
141 // @SuppressWarnings("unchecked")
143 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
144 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
145 : key
.getNamespaceURI();
146 if (element
.hasAttributeNS(namespaceUriOrNull
, key
.getLocalPart())) {
147 String value
= element
.getAttributeNS(namespaceUriOrNull
, key
.getLocalPart());
148 // if (isDefaultAttrTypeRequested(clss))
149 // return Optional.of((A) CrAttributeType.parse(value));
150 return CrAttributeType
.cast(clss
, value
);
152 return Optional
.empty();
156 public Object
put(QName key
, Object value
) {
157 Object previous
= get(key
);
158 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
159 : key
.getNamespaceURI();
160 String prefixToUse
= registerPrefixIfNeeded(key
);
161 element
.setAttributeNS(namespaceUriOrNull
,
162 namespaceUriOrNull
== null ? key
.getLocalPart() : prefixToUse
+ ":" + key
.getLocalPart(),
167 protected String
registerPrefixIfNeeded(QName name
) {
168 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
169 : name
.getNamespaceURI();
171 if (namespaceUriOrNull
!= null) {
172 String registeredPrefix
= provider
.getPrefix(namespaceUriOrNull
);
173 if (registeredPrefix
!= null) {
174 prefixToUse
= registeredPrefix
;
176 provider
.registerPrefix(name
.getPrefix(), namespaceUriOrNull
);
177 prefixToUse
= name
.getPrefix();
186 public boolean hasText() {
187 // return element instanceof Text;
190 NodeList nodeList
= element
.getChildNodes();
191 if (nodeList
.getLength() > 1) {
195 nodes
: for (int i
= 0; i
< nodeList
.getLength(); i
++) {
196 Node node
= nodeList
.item(i
);
197 if (node
instanceof Text
) {
198 Text text
= (Text
) node
;
199 if (!text
.isElementContentWhitespace()) {
210 // text = element.getTextContent();
211 // return text != null;
215 public String
getText() {
217 return element
.getTextContent();
227 public Iterator
<Content
> iterator() {
228 NodeList nodeList
= element
.getChildNodes();
229 return new ElementIterator(this, getSession(), provider
, nodeList
);
233 public Content
getParent() {
234 Node parentNode
= element
.getParentNode();
236 String mountPath
= provider
.getMountPath();
237 if (mountPath
== null)
239 if (Content
.ROOT_PATH
.equals(mountPath
)) {
242 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
243 if (ContentUtils
.EMPTY
.equals(parent
[0]))
245 return getSession().get(parent
[0]);
247 if (!(parentNode
instanceof Element
))
248 throw new IllegalStateException("Parent is not an element");
249 return new DomContent(this, (Element
) parentNode
);
253 public Content
add(QName name
, QName
... classes
) {
254 // TODO consider classes
255 Document document
= this.element
.getOwnerDocument();
256 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
257 : name
.getNamespaceURI();
258 String prefixToUse
= registerPrefixIfNeeded(name
);
259 Element child
= document
.createElementNS(namespaceUriOrNull
,
260 namespaceUriOrNull
== null ? name
.getLocalPart() : prefixToUse
+ ":" + name
.getLocalPart());
261 element
.appendChild(child
);
262 return new DomContent(this, child
);
266 public void remove() {
267 // TODO make it more robust
268 element
.getParentNode().removeChild(element
);
273 protected void removeAttr(QName key
) {
274 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
275 : key
.getNamespaceURI();
276 element
.removeAttributeNS(namespaceUriOrNull
,
277 namespaceUriOrNull
== null ? key
.getLocalPart() : key
.getPrefix() + ":" + key
.getLocalPart());
281 @SuppressWarnings("unchecked")
283 public <A
> A
adapt(Class
<A
> clss
) throws IllegalArgumentException
{
284 if (CharBuffer
.class.isAssignableFrom(clss
)) {
285 String textContent
= element
.getTextContent();
286 CharBuffer buf
= CharBuffer
.wrap(textContent
);
288 } else if (Source
.class.isAssignableFrom(clss
)) {
289 DOMSource source
= new DOMSource(element
);
292 return super.adapt(clss
);
295 @SuppressWarnings("unchecked")
296 public <A
> CompletableFuture
<A
> write(Class
<A
> clss
) {
297 if (String
.class.isAssignableFrom(clss
)) {
298 CompletableFuture
<String
> res
= new CompletableFuture
<>();
299 res
.thenAccept((s
) -> {
300 getSession().notifyModification(this);
301 element
.setTextContent(s
);
303 return (CompletableFuture
<A
>) res
;
304 } else if (Source
.class.isAssignableFrom(clss
)) {
305 CompletableFuture
<Source
> res
= new CompletableFuture
<>();
306 res
.thenAccept((source
) -> {
308 Transformer transformer
= provider
.getTransformerFactory().newTransformer();
309 DocumentFragment documentFragment
= element
.getOwnerDocument().createDocumentFragment();
310 DOMResult result
= new DOMResult(documentFragment
);
311 transformer
.transform(source
, result
);
312 // Node parentNode = element.getParentNode();
313 Element resultElement
= (Element
) documentFragment
.getFirstChild();
314 QName resultName
= toQName(resultElement
);
315 if (!resultName
.equals(getName()))
316 throw new IllegalArgumentException(resultName
+ "+ is not compatible with " + getName());
319 NamedNodeMap attrs
= resultElement
.getAttributes();
320 for (int i
= 0; i
< attrs
.getLength(); i
++) {
321 Attr attr2
= (Attr
) element
.getOwnerDocument().importNode(attrs
.item(i
), true);
322 element
.getAttributes().setNamedItem(attr2
);
325 // Move all the children
326 while (element
.hasChildNodes()) {
327 element
.removeChild(element
.getFirstChild());
329 while (resultElement
.hasChildNodes()) {
330 element
.appendChild(resultElement
.getFirstChild());
332 // parentNode.replaceChild(resultNode, element);
333 // element = (Element)resultNode;
335 } catch (DOMException
| TransformerException e
) {
336 throw new RuntimeException("Cannot write to element", e
);
339 return (CompletableFuture
<A
>) res
;
341 return super.write(clss
);
344 @SuppressWarnings("unchecked")
346 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
347 if (InputStream
.class.isAssignableFrom(clss
)) {
348 PipedOutputStream out
= new PipedOutputStream();
349 ForkJoinPool
.commonPool().execute(() -> {
351 Source source
= new DOMSource(element
);
352 Result result
= new StreamResult(out
);
353 provider
.getTransformerFactory().newTransformer().transform(source
, result
);
356 } catch (TransformerException
| IOException e
) {
357 throw new RuntimeException("Cannot read " + getPath(), e
);
360 return (C
) new PipedInputStream(out
);
362 return super.open(clss
);
366 public int getSiblingIndex() {
367 Node curr
= element
.getPreviousSibling();
369 while (curr
!= null) {
370 if (curr
instanceof Element
) {
371 if (Objects
.equals(curr
.getNamespaceURI(), element
.getNamespaceURI())
372 && Objects
.equals(curr
.getLocalName(), element
.getLocalName())) {
376 curr
= curr
.getPreviousSibling();
385 public List
<QName
> getContentClasses() {
386 List
<QName
> res
= new ArrayList
<>();
388 String mountPath
= provider
.getMountPath();
389 if (Content
.ROOT_PATH
.equals(mountPath
)) {// repository root
390 res
.add(CrName
.root
.qName());
392 Content mountPoint
= getSession().getMountPoint(mountPath
);
393 res
.addAll(mountPoint
.getContentClasses());
402 public void addContentClasses(QName
... contentClass
) {
404 String mountPath
= provider
.getMountPath();
405 if (Content
.ROOT_PATH
.equals(mountPath
)) {// repository root
406 throw new IllegalArgumentException("Cannot add content classes to repository root");
408 Content mountPoint
= getSession().getMountPoint(mountPath
);
409 mountPoint
.addContentClasses(contentClass
);
412 super.addContentClasses(contentClass
);
420 public ProvidedContent
getMountPoint(String relativePath
) {
421 // FIXME use qualified names
422 Element childElement
= (Element
) element
.getElementsByTagName(relativePath
).item(0);
423 // TODO check that it is a mount
424 return new DomContent(this, childElement
);
428 public DomContentProvider
getProvider() {