1 package org
.argeo
.cms
.acr
.xml
;
3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.ByteArrayOutputStream
;
5 import java
.io
.Closeable
;
6 import java
.io
.IOException
;
7 import java
.io
.InputStream
;
8 import java
.io
.PipedInputStream
;
9 import java
.io
.PipedOutputStream
;
10 import java
.nio
.CharBuffer
;
11 import java
.util
.ArrayList
;
12 import java
.util
.HashSet
;
13 import java
.util
.Iterator
;
14 import java
.util
.List
;
15 import java
.util
.Objects
;
16 import java
.util
.Optional
;
18 import java
.util
.concurrent
.CompletableFuture
;
19 import java
.util
.concurrent
.ForkJoinPool
;
21 import javax
.xml
.XMLConstants
;
22 import javax
.xml
.namespace
.NamespaceContext
;
23 import javax
.xml
.namespace
.QName
;
24 import javax
.xml
.transform
.Result
;
25 import javax
.xml
.transform
.Source
;
26 import javax
.xml
.transform
.Transformer
;
27 import javax
.xml
.transform
.TransformerException
;
28 import javax
.xml
.transform
.TransformerFactory
;
29 import javax
.xml
.transform
.dom
.DOMResult
;
30 import javax
.xml
.transform
.dom
.DOMSource
;
31 import javax
.xml
.transform
.stream
.StreamResult
;
33 import org
.argeo
.api
.acr
.Content
;
34 import org
.argeo
.api
.acr
.ContentName
;
35 import org
.argeo
.api
.acr
.CrName
;
36 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
37 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
38 import org
.argeo
.cms
.acr
.AbstractContent
;
39 import org
.argeo
.cms
.acr
.ContentUtils
;
40 import org
.w3c
.dom
.Attr
;
41 import org
.w3c
.dom
.DOMException
;
42 import org
.w3c
.dom
.Document
;
43 import org
.w3c
.dom
.DocumentFragment
;
44 import org
.w3c
.dom
.Element
;
45 import org
.w3c
.dom
.NamedNodeMap
;
46 import org
.w3c
.dom
.Node
;
47 import org
.w3c
.dom
.NodeList
;
48 import org
.w3c
.dom
.Text
;
50 /** Content persisted as a DOM element. */
51 public class DomContent
extends AbstractContent
implements ProvidedContent
{
53 private final DomContentProvider provider
;
54 private final Element element
;
56 // private String text = null;
57 private Boolean hasText
= null;
59 public DomContent(ProvidedSession session
, DomContentProvider contentProvider
, Element element
) {
61 this.provider
= contentProvider
;
62 this.element
= element
;
65 public DomContent(DomContent context
, Element element
) {
66 this(context
.getSession(), context
.getProvider(), element
);
70 public QName
getName() {
71 if (isLocalRoot()) {// root
72 String mountPath
= provider
.getMountPath();
73 if (mountPath
!= null) {
74 if (ContentUtils
.ROOT_SLASH
.equals(mountPath
)) {
75 return CrName
.root
.qName();
77 Content mountPoint
= getSession().getMountPoint(mountPath
);
78 QName mountPointName
= mountPoint
.getName();
79 return mountPointName
;
82 return toQName(this.element
);
85 protected boolean isLocalRoot() {
86 return element
.getParentNode() == null || element
.getParentNode() instanceof Document
;
89 protected QName
toQName(Node node
) {
90 String prefix
= node
.getPrefix();
92 String namespaceURI
= node
.getNamespaceURI();
93 if (namespaceURI
== null)
94 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(null);
95 if (namespaceURI
== null) {
96 return toQName(node
, node
.getLocalName());
98 String contextPrefix
= provider
.getPrefix(namespaceURI
);
99 if (contextPrefix
== null)
100 throw new IllegalStateException("Namespace " + namespaceURI
+ " is unbound");
101 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
104 String namespaceURI
= node
.getNamespaceURI();
105 if (namespaceURI
== null)
106 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(prefix
);
107 if (namespaceURI
== null) {
108 namespaceURI
= provider
.getNamespaceURI(prefix
);
109 if (XMLConstants
.NULL_NS_URI
.equals(namespaceURI
))
110 throw new IllegalStateException("Prefix " + prefix
+ " is unbound");
111 // TODO bind the prefix in the document?
113 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
117 protected QName
toQName(Node source
, String namespaceURI
, String localName
, NamespaceContext namespaceContext
) {
118 return new ContentName(namespaceURI
, localName
, namespaceContext
);
121 protected QName
toQName(Node source
, String localName
) {
122 return new ContentName(localName
);
125 * ATTRIBUTES OPERATIONS
129 public Iterable
<QName
> keys() {
130 // TODO implement an iterator?
131 Set
<QName
> result
= new HashSet
<>();
132 NamedNodeMap attributes
= element
.getAttributes();
133 for (int i
= 0; i
< attributes
.getLength(); i
++) {
134 Attr attr
= (Attr
) attributes
.item(i
);
135 QName key
= toQName(attr
);
136 if (key
.getNamespaceURI().equals(XMLConstants
.XMLNS_ATTRIBUTE_NS_URI
))
137 continue;// skip prefix mapping
143 @SuppressWarnings("unchecked")
145 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
146 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
147 : key
.getNamespaceURI();
148 if (element
.hasAttributeNS(namespaceUriOrNull
, key
.getLocalPart())) {
149 String value
= element
.getAttributeNS(namespaceUriOrNull
, key
.getLocalPart());
150 if (clss
.isAssignableFrom(String
.class))
151 return Optional
.of((A
) value
);
153 return Optional
.empty();
155 return Optional
.empty();
159 public Object
put(QName key
, Object value
) {
160 Object previous
= get(key
);
161 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
162 : key
.getNamespaceURI();
163 String prefixToUse
= registerPrefixIfNeeded(key
);
164 element
.setAttributeNS(namespaceUriOrNull
,
165 namespaceUriOrNull
== null ? key
.getLocalPart() : prefixToUse
+ ":" + key
.getLocalPart(),
170 protected String
registerPrefixIfNeeded(QName name
) {
171 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
172 : name
.getNamespaceURI();
174 if (namespaceUriOrNull
!= null) {
175 String registeredPrefix
= provider
.getPrefix(namespaceUriOrNull
);
176 if (registeredPrefix
!= null) {
177 prefixToUse
= registeredPrefix
;
179 provider
.registerPrefix(name
.getPrefix(), namespaceUriOrNull
);
180 prefixToUse
= name
.getPrefix();
189 public boolean hasText() {
190 // return element instanceof Text;
193 NodeList nodeList
= element
.getChildNodes();
194 if (nodeList
.getLength() > 1) {
198 nodes
: for (int i
= 0; i
< nodeList
.getLength(); i
++) {
199 Node node
= nodeList
.item(i
);
200 if (node
instanceof Text
) {
201 Text text
= (Text
) node
;
202 if (!text
.isElementContentWhitespace()) {
213 // text = element.getTextContent();
214 // return text != null;
218 public String
getText() {
220 return element
.getTextContent();
230 public Iterator
<Content
> iterator() {
231 NodeList nodeList
= element
.getChildNodes();
232 return new ElementIterator(this, getSession(), provider
, nodeList
);
236 public Content
getParent() {
237 Node parentNode
= element
.getParentNode();
239 String mountPath
= provider
.getMountPath();
240 if (mountPath
== null)
242 if (ContentUtils
.ROOT_SLASH
.equals(mountPath
)) {
245 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
246 if (ContentUtils
.EMPTY
.equals(parent
[0]))
248 return getSession().get(parent
[0]);
250 if (!(parentNode
instanceof Element
))
251 throw new IllegalStateException("Parent is not an element");
252 return new DomContent(this, (Element
) parentNode
);
256 public Content
add(QName name
, QName
... classes
) {
257 // TODO consider classes
258 Document document
= this.element
.getOwnerDocument();
259 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
260 : name
.getNamespaceURI();
261 String prefixToUse
= registerPrefixIfNeeded(name
);
262 Element child
= document
.createElementNS(namespaceUriOrNull
,
263 namespaceUriOrNull
== null ? name
.getLocalPart() : prefixToUse
+ ":" + name
.getLocalPart());
264 element
.appendChild(child
);
265 return new DomContent(this, child
);
269 public void remove() {
270 // TODO make it more robust
271 element
.getParentNode().removeChild(element
);
276 protected void removeAttr(QName key
) {
277 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
278 : key
.getNamespaceURI();
279 element
.removeAttributeNS(namespaceUriOrNull
,
280 namespaceUriOrNull
== null ? key
.getLocalPart() : key
.getPrefix() + ":" + key
.getLocalPart());
284 @SuppressWarnings("unchecked")
286 public <A
> A
adapt(Class
<A
> clss
) throws IllegalArgumentException
{
287 if (CharBuffer
.class.isAssignableFrom(clss
)) {
288 String textContent
= element
.getTextContent();
289 CharBuffer buf
= CharBuffer
.wrap(textContent
);
291 } else if (Source
.class.isAssignableFrom(clss
)) {
292 DOMSource source
= new DOMSource(element
);
295 return super.adapt(clss
);
298 @SuppressWarnings("unchecked")
299 public <A
> CompletableFuture
<A
> write(Class
<A
> clss
) {
300 if (String
.class.isAssignableFrom(clss
)) {
301 CompletableFuture
<String
> res
= new CompletableFuture
<>();
302 res
.thenAccept((s
) -> {
303 getSession().notifyModification(this);
304 element
.setTextContent(s
);
306 return (CompletableFuture
<A
>) res
;
307 } else if (Source
.class.isAssignableFrom(clss
)) {
308 CompletableFuture
<Source
> res
= new CompletableFuture
<>();
309 res
.thenAccept((source
) -> {
311 Transformer transformer
= provider
.getTransformerFactory().newTransformer();
312 DocumentFragment documentFragment
= element
.getOwnerDocument().createDocumentFragment();
313 DOMResult result
= new DOMResult(documentFragment
);
314 transformer
.transform(source
, result
);
315 // Node parentNode = element.getParentNode();
316 Element resultElement
= (Element
) documentFragment
.getFirstChild();
317 QName resultName
= toQName(resultElement
);
318 if (!resultName
.equals(getName()))
319 throw new IllegalArgumentException(resultName
+ "+ is not compatible with " + getName());
322 NamedNodeMap attrs
= resultElement
.getAttributes();
323 for (int i
= 0; i
< attrs
.getLength(); i
++) {
324 Attr attr2
= (Attr
) element
.getOwnerDocument().importNode(attrs
.item(i
), true);
325 element
.getAttributes().setNamedItem(attr2
);
328 // Move all the children
329 while (element
.hasChildNodes()) {
330 element
.removeChild(element
.getFirstChild());
332 while (resultElement
.hasChildNodes()) {
333 element
.appendChild(resultElement
.getFirstChild());
335 // parentNode.replaceChild(resultNode, element);
336 // element = (Element)resultNode;
338 } catch (DOMException
| TransformerException e
) {
339 throw new RuntimeException("Cannot write to element", e
);
342 return (CompletableFuture
<A
>) res
;
344 return super.write(clss
);
347 @SuppressWarnings("unchecked")
349 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
350 if (InputStream
.class.isAssignableFrom(clss
)) {
351 PipedOutputStream out
= new PipedOutputStream();
352 ForkJoinPool
.commonPool().execute(() -> {
354 Source source
= new DOMSource(element
);
355 Result result
= new StreamResult(out
);
356 provider
.getTransformerFactory().newTransformer().transform(source
, result
);
359 } catch (TransformerException
| IOException e
) {
360 throw new RuntimeException("Cannot read " + getPath(), e
);
363 return (C
) new PipedInputStream(out
);
365 return super.open(clss
);
369 public int getSiblingIndex() {
370 Node curr
= element
.getPreviousSibling();
372 while (curr
!= null) {
373 if (curr
instanceof Element
) {
374 if (Objects
.equals(curr
.getNamespaceURI(), element
.getNamespaceURI())
375 && Objects
.equals(curr
.getLocalName(), element
.getLocalName())) {
379 curr
= curr
.getPreviousSibling();
388 public List
<QName
> getContentClasses() {
389 List
<QName
> res
= new ArrayList
<>();
391 String mountPath
= provider
.getMountPath();
392 if (mountPath
!= null) {
393 Content mountPoint
= getSession().getMountPoint(mountPath
);
394 res
.addAll(mountPoint
.getContentClasses());
403 public void addContentClasses(QName
... contentClass
) {
405 String mountPath
= provider
.getMountPath();
406 if (mountPath
!= null) {
407 Content mountPoint
= getSession().getMountPoint(mountPath
);
408 mountPoint
.addContentClasses(contentClass
);
411 super.addContentClasses(contentClass
);
419 public ProvidedContent
getMountPoint(String relativePath
) {
420 // FIXME use qualified names
421 Element childElement
= (Element
) element
.getElementsByTagName(relativePath
).item(0);
422 // TODO check that it is a mount
423 return new DomContent(this, childElement
);
427 public DomContentProvider
getProvider() {