1 package org
.argeo
.cms
.acr
.xml
;
3 import java
.nio
.CharBuffer
;
4 import java
.util
.ArrayList
;
5 import java
.util
.HashSet
;
6 import java
.util
.Iterator
;
8 import java
.util
.Optional
;
10 import java
.util
.concurrent
.CompletableFuture
;
12 import javax
.xml
.XMLConstants
;
13 import javax
.xml
.namespace
.NamespaceContext
;
14 import javax
.xml
.namespace
.QName
;
15 import javax
.xml
.transform
.Source
;
16 import javax
.xml
.transform
.Transformer
;
17 import javax
.xml
.transform
.TransformerException
;
18 import javax
.xml
.transform
.dom
.DOMResult
;
19 import javax
.xml
.transform
.dom
.DOMSource
;
21 import org
.argeo
.api
.acr
.Content
;
22 import org
.argeo
.api
.acr
.ContentName
;
23 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
24 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
25 import org
.argeo
.cms
.acr
.AbstractContent
;
26 import org
.argeo
.cms
.acr
.ContentUtils
;
27 import org
.w3c
.dom
.Attr
;
28 import org
.w3c
.dom
.DOMException
;
29 import org
.w3c
.dom
.Document
;
30 import org
.w3c
.dom
.DocumentFragment
;
31 import org
.w3c
.dom
.Element
;
32 import org
.w3c
.dom
.NamedNodeMap
;
33 import org
.w3c
.dom
.Node
;
34 import org
.w3c
.dom
.NodeList
;
35 import org
.w3c
.dom
.Text
;
37 /** Content persisted as a DOM element. */
38 public class DomContent
extends AbstractContent
implements ProvidedContent
{
40 private final DomContentProvider provider
;
41 private final Element element
;
43 // private String text = null;
44 private Boolean hasText
= null;
46 public DomContent(ProvidedSession session
, DomContentProvider contentProvider
, Element element
) {
48 this.provider
= contentProvider
;
49 this.element
= element
;
52 public DomContent(DomContent context
, Element element
) {
53 this(context
.getSession(), context
.getProvider(), element
);
57 public QName
getName() {
58 if (element
.getParentNode() == null) {// root
59 String mountPath
= provider
.getMountPath();
60 if (mountPath
!= null) {
61 Content mountPoint
= getSession().getMountPoint(mountPath
);
62 return mountPoint
.getName();
65 return toQName(this.element
);
68 protected QName
toQName(Node node
) {
69 String prefix
= node
.getPrefix();
71 String namespaceURI
= node
.getNamespaceURI();
72 if (namespaceURI
== null)
73 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(null);
74 if (namespaceURI
== null) {
75 return toQName(node
, node
.getLocalName());
77 String contextPrefix
= provider
.getPrefix(namespaceURI
);
78 if (contextPrefix
== null)
79 throw new IllegalStateException("Namespace " + namespaceURI
+ " is unbound");
80 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
83 String namespaceURI
= node
.getNamespaceURI();
84 if (namespaceURI
== null)
85 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(prefix
);
86 if (namespaceURI
== null) {
87 namespaceURI
= provider
.getNamespaceURI(prefix
);
88 if (XMLConstants
.NULL_NS_URI
.equals(namespaceURI
))
89 throw new IllegalStateException("Prefix " + prefix
+ " is unbound");
90 // TODO bind the prefix in the document?
92 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
96 protected QName
toQName(Node source
, String namespaceURI
, String localName
, NamespaceContext namespaceContext
) {
97 return new ContentName(namespaceURI
, localName
, namespaceContext
);
100 protected QName
toQName(Node source
, String localName
) {
101 return new ContentName(localName
);
104 * ATTRIBUTES OPERATIONS
108 public Iterable
<QName
> keys() {
109 // TODO implement an iterator?
110 Set
<QName
> result
= new HashSet
<>();
111 NamedNodeMap attributes
= element
.getAttributes();
112 for (int i
= 0; i
< attributes
.getLength(); i
++) {
113 Attr attr
= (Attr
) attributes
.item(i
);
114 QName key
= toQName(attr
);
120 @SuppressWarnings("unchecked")
122 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
123 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
124 : key
.getNamespaceURI();
125 if (element
.hasAttributeNS(namespaceUriOrNull
, key
.getLocalPart())) {
126 String value
= element
.getAttributeNS(namespaceUriOrNull
, key
.getLocalPart());
127 if (clss
.isAssignableFrom(String
.class))
128 return Optional
.of((A
) value
);
130 return Optional
.empty();
136 public Object
put(QName key
, Object value
) {
137 Object previous
= get(key
);
138 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
139 : key
.getNamespaceURI();
140 String prefixToUse
= registerPrefixIfNeeded(key
);
141 element
.setAttributeNS(namespaceUriOrNull
,
142 namespaceUriOrNull
== null ? key
.getLocalPart() : prefixToUse
+ ":" + key
.getLocalPart(),
147 protected String
registerPrefixIfNeeded(QName name
) {
148 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
149 : name
.getNamespaceURI();
151 if (namespaceUriOrNull
!= null) {
152 String registeredPrefix
= provider
.getPrefix(namespaceUriOrNull
);
153 if (registeredPrefix
!= null) {
154 prefixToUse
= registeredPrefix
;
156 provider
.registerPrefix(name
.getPrefix(), namespaceUriOrNull
);
157 prefixToUse
= name
.getPrefix();
166 public boolean hasText() {
167 // return element instanceof Text;
170 NodeList nodeList
= element
.getChildNodes();
171 if (nodeList
.getLength() > 1) {
175 nodes
: for (int i
= 0; i
< nodeList
.getLength(); i
++) {
176 Node node
= nodeList
.item(i
);
177 if (node
instanceof Text
) {
178 Text text
= (Text
) node
;
179 if (!text
.isElementContentWhitespace()) {
190 // text = element.getTextContent();
191 // return text != null;
195 public String
getText() {
197 return element
.getTextContent();
207 public Iterator
<Content
> iterator() {
208 NodeList nodeList
= element
.getChildNodes();
209 return new ElementIterator(this, getSession(), provider
, nodeList
);
213 public Content
getParent() {
214 Node parentNode
= element
.getParentNode();
215 if (parentNode
== null) {
216 String mountPath
= provider
.getMountPath();
217 if (mountPath
== null)
219 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
220 return getSession().get(parent
[0]);
222 if (parentNode
instanceof Document
)
224 if (!(parentNode
instanceof Element
))
225 throw new IllegalStateException("Parent is not an element");
226 return new DomContent(this, (Element
) parentNode
);
230 public Content
add(QName name
, QName
... classes
) {
231 // TODO consider classes
232 Document document
= this.element
.getOwnerDocument();
233 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
234 : name
.getNamespaceURI();
235 String prefixToUse
= registerPrefixIfNeeded(name
);
236 Element child
= document
.createElementNS(namespaceUriOrNull
,
237 namespaceUriOrNull
== null ? name
.getLocalPart() : prefixToUse
+ ":" + name
.getLocalPart());
238 element
.appendChild(child
);
239 return new DomContent(this, child
);
243 public void remove() {
244 // TODO make it more robust
245 element
.getParentNode().removeChild(element
);
250 protected void removeAttr(QName key
) {
251 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
252 : key
.getNamespaceURI();
253 element
.removeAttributeNS(namespaceUriOrNull
,
254 namespaceUriOrNull
== null ? key
.getLocalPart() : key
.getPrefix() + ":" + key
.getLocalPart());
258 @SuppressWarnings("unchecked")
260 public <A
> A
adapt(Class
<A
> clss
) throws IllegalArgumentException
{
261 if (CharBuffer
.class.isAssignableFrom(clss
)) {
262 String textContent
= element
.getTextContent();
263 CharBuffer buf
= CharBuffer
.wrap(textContent
);
265 } else if (Source
.class.isAssignableFrom(clss
)) {
266 DOMSource source
= new DOMSource(element
);
269 return super.adapt(clss
);
272 @SuppressWarnings("unchecked")
273 public <A
> CompletableFuture
<A
> write(Class
<A
> clss
) {
274 if (String
.class.isAssignableFrom(clss
)) {
275 CompletableFuture
<String
> res
= new CompletableFuture
<>();
276 res
.thenAccept((s
) -> {
277 getSession().notifyModification(this);
278 element
.setTextContent(s
);
280 return (CompletableFuture
<A
>) res
;
281 } else if (Source
.class.isAssignableFrom(clss
)) {
282 CompletableFuture
<Source
> res
= new CompletableFuture
<>();
283 res
.thenAccept((source
) -> {
285 Transformer transformer
= provider
.getTransformerFactory().newTransformer();
286 DocumentFragment documentFragment
= element
.getOwnerDocument().createDocumentFragment();
287 DOMResult result
= new DOMResult(documentFragment
);
288 transformer
.transform(source
, result
);
289 // Node parentNode = element.getParentNode();
290 Element resultElement
= (Element
) documentFragment
.getFirstChild();
291 QName resultName
= toQName(resultElement
);
292 if (!resultName
.equals(getName()))
293 throw new IllegalArgumentException(resultName
+ "+ is not compatible with " + getName());
296 NamedNodeMap attrs
= resultElement
.getAttributes();
297 for (int i
= 0; i
< attrs
.getLength(); i
++) {
298 Attr attr2
= (Attr
) element
.getOwnerDocument().importNode(attrs
.item(i
), true);
299 element
.getAttributes().setNamedItem(attr2
);
302 // Move all the children
303 while (element
.hasChildNodes()) {
304 element
.removeChild(element
.getFirstChild());
306 while (resultElement
.hasChildNodes()) {
307 element
.appendChild(resultElement
.getFirstChild());
309 // parentNode.replaceChild(resultNode, element);
310 // element = (Element)resultNode;
312 } catch (DOMException
| TransformerException e
) {
313 throw new RuntimeException("Cannot write to element", e
);
316 return (CompletableFuture
<A
>) res
;
318 return super.write(clss
);
325 public List
<QName
> getContentClasses() {
326 List
<QName
> res
= new ArrayList
<>();
335 public ProvidedContent
getMountPoint(String relativePath
) {
336 // FIXME use qualified names
337 Element childElement
= (Element
) element
.getElementsByTagName(relativePath
).item(0);
338 // TODO check that it is a mount
339 return new DomContent(this, childElement
);
343 public DomContentProvider
getProvider() {