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
.Objects
;
9 import java
.util
.Optional
;
11 import java
.util
.concurrent
.CompletableFuture
;
13 import javax
.xml
.XMLConstants
;
14 import javax
.xml
.namespace
.NamespaceContext
;
15 import javax
.xml
.namespace
.QName
;
16 import javax
.xml
.transform
.Source
;
17 import javax
.xml
.transform
.Transformer
;
18 import javax
.xml
.transform
.TransformerException
;
19 import javax
.xml
.transform
.dom
.DOMResult
;
20 import javax
.xml
.transform
.dom
.DOMSource
;
22 import org
.argeo
.api
.acr
.Content
;
23 import org
.argeo
.api
.acr
.ContentName
;
24 import org
.argeo
.api
.acr
.CrName
;
25 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
26 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
27 import org
.argeo
.cms
.acr
.AbstractContent
;
28 import org
.argeo
.cms
.acr
.ContentUtils
;
29 import org
.w3c
.dom
.Attr
;
30 import org
.w3c
.dom
.DOMException
;
31 import org
.w3c
.dom
.Document
;
32 import org
.w3c
.dom
.DocumentFragment
;
33 import org
.w3c
.dom
.Element
;
34 import org
.w3c
.dom
.NamedNodeMap
;
35 import org
.w3c
.dom
.Node
;
36 import org
.w3c
.dom
.NodeList
;
37 import org
.w3c
.dom
.Text
;
39 /** Content persisted as a DOM element. */
40 public class DomContent
extends AbstractContent
implements ProvidedContent
{
42 private final DomContentProvider provider
;
43 private final Element element
;
45 // private String text = null;
46 private Boolean hasText
= null;
48 public DomContent(ProvidedSession session
, DomContentProvider contentProvider
, Element element
) {
50 this.provider
= contentProvider
;
51 this.element
= element
;
54 public DomContent(DomContent context
, Element element
) {
55 this(context
.getSession(), context
.getProvider(), element
);
59 public QName
getName() {
60 if (isLocalRoot()) {// root
61 String mountPath
= provider
.getMountPath();
62 if (mountPath
!= null) {
63 if (ContentUtils
.ROOT_SLASH
.equals(mountPath
)) {
64 return CrName
.root
.qName();
66 Content mountPoint
= getSession().getMountPoint(mountPath
);
67 QName mountPointName
= mountPoint
.getName();
68 return mountPointName
;
71 return toQName(this.element
);
74 protected boolean isLocalRoot() {
75 return element
.getParentNode() == null || element
.getParentNode() instanceof Document
;
78 protected QName
toQName(Node node
) {
79 String prefix
= node
.getPrefix();
81 String namespaceURI
= node
.getNamespaceURI();
82 if (namespaceURI
== null)
83 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(null);
84 if (namespaceURI
== null) {
85 return toQName(node
, node
.getLocalName());
87 String contextPrefix
= provider
.getPrefix(namespaceURI
);
88 if (contextPrefix
== null)
89 throw new IllegalStateException("Namespace " + namespaceURI
+ " is unbound");
90 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
93 String namespaceURI
= node
.getNamespaceURI();
94 if (namespaceURI
== null)
95 namespaceURI
= node
.getOwnerDocument().lookupNamespaceURI(prefix
);
96 if (namespaceURI
== null) {
97 namespaceURI
= provider
.getNamespaceURI(prefix
);
98 if (XMLConstants
.NULL_NS_URI
.equals(namespaceURI
))
99 throw new IllegalStateException("Prefix " + prefix
+ " is unbound");
100 // TODO bind the prefix in the document?
102 return toQName(node
, namespaceURI
, node
.getLocalName(), provider
);
106 protected QName
toQName(Node source
, String namespaceURI
, String localName
, NamespaceContext namespaceContext
) {
107 return new ContentName(namespaceURI
, localName
, namespaceContext
);
110 protected QName
toQName(Node source
, String localName
) {
111 return new ContentName(localName
);
114 * ATTRIBUTES OPERATIONS
118 public Iterable
<QName
> keys() {
119 // TODO implement an iterator?
120 Set
<QName
> result
= new HashSet
<>();
121 NamedNodeMap attributes
= element
.getAttributes();
122 for (int i
= 0; i
< attributes
.getLength(); i
++) {
123 Attr attr
= (Attr
) attributes
.item(i
);
124 QName key
= toQName(attr
);
125 if (key
.getNamespaceURI().equals(XMLConstants
.XMLNS_ATTRIBUTE_NS_URI
))
126 continue;// skip prefix mapping
132 @SuppressWarnings("unchecked")
134 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
135 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
136 : key
.getNamespaceURI();
137 if (element
.hasAttributeNS(namespaceUriOrNull
, key
.getLocalPart())) {
138 String value
= element
.getAttributeNS(namespaceUriOrNull
, key
.getLocalPart());
139 if (clss
.isAssignableFrom(String
.class))
140 return Optional
.of((A
) value
);
142 return Optional
.empty();
144 return Optional
.empty();
148 public Object
put(QName key
, Object value
) {
149 Object previous
= get(key
);
150 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
151 : key
.getNamespaceURI();
152 String prefixToUse
= registerPrefixIfNeeded(key
);
153 element
.setAttributeNS(namespaceUriOrNull
,
154 namespaceUriOrNull
== null ? key
.getLocalPart() : prefixToUse
+ ":" + key
.getLocalPart(),
159 protected String
registerPrefixIfNeeded(QName name
) {
160 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
161 : name
.getNamespaceURI();
163 if (namespaceUriOrNull
!= null) {
164 String registeredPrefix
= provider
.getPrefix(namespaceUriOrNull
);
165 if (registeredPrefix
!= null) {
166 prefixToUse
= registeredPrefix
;
168 provider
.registerPrefix(name
.getPrefix(), namespaceUriOrNull
);
169 prefixToUse
= name
.getPrefix();
178 public boolean hasText() {
179 // return element instanceof Text;
182 NodeList nodeList
= element
.getChildNodes();
183 if (nodeList
.getLength() > 1) {
187 nodes
: for (int i
= 0; i
< nodeList
.getLength(); i
++) {
188 Node node
= nodeList
.item(i
);
189 if (node
instanceof Text
) {
190 Text text
= (Text
) node
;
191 if (!text
.isElementContentWhitespace()) {
202 // text = element.getTextContent();
203 // return text != null;
207 public String
getText() {
209 return element
.getTextContent();
219 public Iterator
<Content
> iterator() {
220 NodeList nodeList
= element
.getChildNodes();
221 return new ElementIterator(this, getSession(), provider
, nodeList
);
225 public Content
getParent() {
226 Node parentNode
= element
.getParentNode();
228 String mountPath
= provider
.getMountPath();
229 if (mountPath
== null)
231 if (ContentUtils
.ROOT_SLASH
.equals(mountPath
)) {
234 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
235 if (ContentUtils
.EMPTY
.equals(parent
[0]))
237 return getSession().get(parent
[0]);
239 if (!(parentNode
instanceof Element
))
240 throw new IllegalStateException("Parent is not an element");
241 return new DomContent(this, (Element
) parentNode
);
245 public Content
add(QName name
, QName
... classes
) {
246 // TODO consider classes
247 Document document
= this.element
.getOwnerDocument();
248 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(name
.getNamespaceURI()) ?
null
249 : name
.getNamespaceURI();
250 String prefixToUse
= registerPrefixIfNeeded(name
);
251 Element child
= document
.createElementNS(namespaceUriOrNull
,
252 namespaceUriOrNull
== null ? name
.getLocalPart() : prefixToUse
+ ":" + name
.getLocalPart());
253 element
.appendChild(child
);
254 return new DomContent(this, child
);
258 public void remove() {
259 // TODO make it more robust
260 element
.getParentNode().removeChild(element
);
265 protected void removeAttr(QName key
) {
266 String namespaceUriOrNull
= XMLConstants
.NULL_NS_URI
.equals(key
.getNamespaceURI()) ?
null
267 : key
.getNamespaceURI();
268 element
.removeAttributeNS(namespaceUriOrNull
,
269 namespaceUriOrNull
== null ? key
.getLocalPart() : key
.getPrefix() + ":" + key
.getLocalPart());
273 @SuppressWarnings("unchecked")
275 public <A
> A
adapt(Class
<A
> clss
) throws IllegalArgumentException
{
276 if (CharBuffer
.class.isAssignableFrom(clss
)) {
277 String textContent
= element
.getTextContent();
278 CharBuffer buf
= CharBuffer
.wrap(textContent
);
280 } else if (Source
.class.isAssignableFrom(clss
)) {
281 DOMSource source
= new DOMSource(element
);
284 return super.adapt(clss
);
287 @SuppressWarnings("unchecked")
288 public <A
> CompletableFuture
<A
> write(Class
<A
> clss
) {
289 if (String
.class.isAssignableFrom(clss
)) {
290 CompletableFuture
<String
> res
= new CompletableFuture
<>();
291 res
.thenAccept((s
) -> {
292 getSession().notifyModification(this);
293 element
.setTextContent(s
);
295 return (CompletableFuture
<A
>) res
;
296 } else if (Source
.class.isAssignableFrom(clss
)) {
297 CompletableFuture
<Source
> res
= new CompletableFuture
<>();
298 res
.thenAccept((source
) -> {
300 Transformer transformer
= provider
.getTransformerFactory().newTransformer();
301 DocumentFragment documentFragment
= element
.getOwnerDocument().createDocumentFragment();
302 DOMResult result
= new DOMResult(documentFragment
);
303 transformer
.transform(source
, result
);
304 // Node parentNode = element.getParentNode();
305 Element resultElement
= (Element
) documentFragment
.getFirstChild();
306 QName resultName
= toQName(resultElement
);
307 if (!resultName
.equals(getName()))
308 throw new IllegalArgumentException(resultName
+ "+ is not compatible with " + getName());
311 NamedNodeMap attrs
= resultElement
.getAttributes();
312 for (int i
= 0; i
< attrs
.getLength(); i
++) {
313 Attr attr2
= (Attr
) element
.getOwnerDocument().importNode(attrs
.item(i
), true);
314 element
.getAttributes().setNamedItem(attr2
);
317 // Move all the children
318 while (element
.hasChildNodes()) {
319 element
.removeChild(element
.getFirstChild());
321 while (resultElement
.hasChildNodes()) {
322 element
.appendChild(resultElement
.getFirstChild());
324 // parentNode.replaceChild(resultNode, element);
325 // element = (Element)resultNode;
327 } catch (DOMException
| TransformerException e
) {
328 throw new RuntimeException("Cannot write to element", e
);
331 return (CompletableFuture
<A
>) res
;
333 return super.write(clss
);
337 public int getSiblingIndex() {
338 Node curr
= element
.getPreviousSibling();
340 while (curr
!= null) {
341 if (curr
instanceof Element
) {
342 if (Objects
.equals(curr
.getNamespaceURI(), element
.getNamespaceURI())
343 && Objects
.equals(curr
.getLocalName(), element
.getLocalName())) {
347 curr
= curr
.getPreviousSibling();
356 public List
<QName
> getContentClasses() {
357 List
<QName
> res
= new ArrayList
<>();
359 String mountPath
= provider
.getMountPath();
360 if (mountPath
!= null) {
361 Content mountPoint
= getSession().getMountPoint(mountPath
);
362 res
.addAll(mountPoint
.getContentClasses());
371 public void addContentClasses(QName
... contentClass
) {
373 String mountPath
= provider
.getMountPath();
374 if (mountPath
!= null) {
375 Content mountPoint
= getSession().getMountPoint(mountPath
);
376 mountPoint
.addContentClasses(contentClass
);
379 super.addContentClasses(contentClass
);
387 public ProvidedContent
getMountPoint(String relativePath
) {
388 // FIXME use qualified names
389 Element childElement
= (Element
) element
.getElementsByTagName(relativePath
).item(0);
390 // TODO check that it is a mount
391 return new DomContent(this, childElement
);
395 public DomContentProvider
getProvider() {