]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java
Refactor WebDav implementation
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / acr / xml / DomContent.java
1 package org.argeo.cms.acr.xml;
2
3 import java.nio.CharBuffer;
4 import java.util.ArrayList;
5 import java.util.HashSet;
6 import java.util.Iterator;
7 import java.util.List;
8 import java.util.Objects;
9 import java.util.Optional;
10 import java.util.Set;
11 import java.util.concurrent.CompletableFuture;
12
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;
21
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;
38
39 /** Content persisted as a DOM element. */
40 public class DomContent extends AbstractContent implements ProvidedContent {
41
42 private final DomContentProvider provider;
43 private final Element element;
44
45 // private String text = null;
46 private Boolean hasText = null;
47
48 public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) {
49 super(session);
50 this.provider = contentProvider;
51 this.element = element;
52 }
53
54 public DomContent(DomContent context, Element element) {
55 this(context.getSession(), context.getProvider(), element);
56 }
57
58 @Override
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();
65 }
66 Content mountPoint = getSession().getMountPoint(mountPath);
67 QName mountPointName = mountPoint.getName();
68 return mountPointName;
69 }
70 }
71 return toQName(this.element);
72 }
73
74 protected boolean isLocalRoot() {
75 return element.getParentNode() == null || element.getParentNode() instanceof Document;
76 }
77
78 protected QName toQName(Node node) {
79 String prefix = node.getPrefix();
80 if (prefix == null) {
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());
86 } else {
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);
91 }
92 } else {
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?
101 }
102 return toQName(node, namespaceURI, node.getLocalName(), provider);
103 }
104 }
105
106 protected QName toQName(Node source, String namespaceURI, String localName, NamespaceContext namespaceContext) {
107 return new ContentName(namespaceURI, localName, namespaceContext);
108 }
109
110 protected QName toQName(Node source, String localName) {
111 return new ContentName(localName);
112 }
113 /*
114 * ATTRIBUTES OPERATIONS
115 */
116
117 @Override
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
127 result.add(key);
128 }
129 return result;
130 }
131
132 @SuppressWarnings("unchecked")
133 @Override
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);
141 else
142 return Optional.empty();
143 } else
144 return Optional.empty();
145 }
146
147 @Override
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(),
155 value.toString());
156 return previous;
157 }
158
159 protected String registerPrefixIfNeeded(QName name) {
160 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
161 : name.getNamespaceURI();
162 String prefixToUse;
163 if (namespaceUriOrNull != null) {
164 String registeredPrefix = provider.getPrefix(namespaceUriOrNull);
165 if (registeredPrefix != null) {
166 prefixToUse = registeredPrefix;
167 } else {
168 provider.registerPrefix(name.getPrefix(), namespaceUriOrNull);
169 prefixToUse = name.getPrefix();
170 }
171 } else {
172 prefixToUse = null;
173 }
174 return prefixToUse;
175 }
176
177 @Override
178 public boolean hasText() {
179 // return element instanceof Text;
180 if (hasText != null)
181 return hasText;
182 NodeList nodeList = element.getChildNodes();
183 if (nodeList.getLength() > 1) {
184 hasText = false;
185 return hasText;
186 }
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()) {
192 hasText = true;
193 break nodes;
194 }
195 }
196 }
197 if (hasText == null)
198 hasText = false;
199 return hasText;
200 // if (text != null)
201 // return true;
202 // text = element.getTextContent();
203 // return text != null;
204 }
205
206 @Override
207 public String getText() {
208 if (hasText())
209 return element.getTextContent();
210 else
211 return null;
212 }
213
214 /*
215 * CONTENT OPERATIONS
216 */
217
218 @Override
219 public Iterator<Content> iterator() {
220 NodeList nodeList = element.getChildNodes();
221 return new ElementIterator(this, getSession(), provider, nodeList);
222 }
223
224 @Override
225 public Content getParent() {
226 Node parentNode = element.getParentNode();
227 if (isLocalRoot()) {
228 String mountPath = provider.getMountPath();
229 if (mountPath == null)
230 return null;
231 if (ContentUtils.ROOT_SLASH.equals(mountPath)) {
232 return null;
233 }
234 String[] parent = ContentUtils.getParentPath(mountPath);
235 if (ContentUtils.EMPTY.equals(parent[0]))
236 return null;
237 return getSession().get(parent[0]);
238 }
239 if (!(parentNode instanceof Element))
240 throw new IllegalStateException("Parent is not an element");
241 return new DomContent(this, (Element) parentNode);
242 }
243
244 @Override
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);
255 }
256
257 @Override
258 public void remove() {
259 // TODO make it more robust
260 element.getParentNode().removeChild(element);
261
262 }
263
264 @Override
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());
270
271 }
272
273 @SuppressWarnings("unchecked")
274 @Override
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);
279 return (A) buf;
280 } else if (Source.class.isAssignableFrom(clss)) {
281 DOMSource source = new DOMSource(element);
282 return (A) source;
283 }
284 return super.adapt(clss);
285 }
286
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);
294 });
295 return (CompletableFuture<A>) res;
296 } else if (Source.class.isAssignableFrom(clss)) {
297 CompletableFuture<Source> res = new CompletableFuture<>();
298 res.thenAccept((source) -> {
299 try {
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());
309
310 // attributes
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);
315 }
316
317 // Move all the children
318 while (element.hasChildNodes()) {
319 element.removeChild(element.getFirstChild());
320 }
321 while (resultElement.hasChildNodes()) {
322 element.appendChild(resultElement.getFirstChild());
323 }
324 // parentNode.replaceChild(resultNode, element);
325 // element = (Element)resultNode;
326
327 } catch (DOMException | TransformerException e) {
328 throw new RuntimeException("Cannot write to element", e);
329 }
330 });
331 return (CompletableFuture<A>) res;
332 }
333 return super.write(clss);
334 }
335
336 @Override
337 public int getSiblingIndex() {
338 Node curr = element.getPreviousSibling();
339 int count = 1;
340 while (curr != null) {
341 if (curr instanceof Element) {
342 if (Objects.equals(curr.getNamespaceURI(), element.getNamespaceURI())
343 && Objects.equals(curr.getLocalName(), element.getLocalName())) {
344 count++;
345 }
346 }
347 curr = curr.getPreviousSibling();
348 }
349 return count;
350 }
351
352 /*
353 * TYPING
354 */
355 @Override
356 public List<QName> getContentClasses() {
357 List<QName> res = new ArrayList<>();
358 if (isLocalRoot()) {
359 String mountPath = provider.getMountPath();
360 if (mountPath != null) {
361 Content mountPoint = getSession().getMountPoint(mountPath);
362 res.addAll(mountPoint.getContentClasses());
363 }
364 } else {
365 res.add(getName());
366 }
367 return res;
368 }
369
370 @Override
371 public void addContentClasses(QName... contentClass) {
372 if (isLocalRoot()) {
373 String mountPath = provider.getMountPath();
374 if (mountPath != null) {
375 Content mountPoint = getSession().getMountPoint(mountPath);
376 mountPoint.addContentClasses(contentClass);
377 }
378 } else {
379 super.addContentClasses(contentClass);
380 }
381 }
382
383 /*
384 * MOUNT MANAGEMENT
385 */
386 @Override
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);
392 }
393
394 @Override
395 public DomContentProvider getProvider() {
396 return provider;
397 }
398
399 }