]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java
Introduce CMS JShell
[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.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;
17 import java.util.Set;
18 import java.util.concurrent.CompletableFuture;
19 import java.util.concurrent.ForkJoinPool;
20
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;
32
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;
49
50 /** Content persisted as a DOM element. */
51 public class DomContent extends AbstractContent implements ProvidedContent {
52
53 private final DomContentProvider provider;
54 private final Element element;
55
56 // private String text = null;
57 private Boolean hasText = null;
58
59 public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) {
60 super(session);
61 this.provider = contentProvider;
62 this.element = element;
63 }
64
65 public DomContent(DomContent context, Element element) {
66 this(context.getSession(), context.getProvider(), element);
67 }
68
69 @Override
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();
76 }
77 Content mountPoint = getSession().getMountPoint(mountPath);
78 QName mountPointName = mountPoint.getName();
79 return mountPointName;
80 }
81 }
82 return toQName(this.element);
83 }
84
85 protected boolean isLocalRoot() {
86 return element.getParentNode() == null || element.getParentNode() instanceof Document;
87 }
88
89 protected QName toQName(Node node) {
90 String prefix = node.getPrefix();
91 if (prefix == null) {
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());
97 } else {
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);
102 }
103 } else {
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?
112 }
113 return toQName(node, namespaceURI, node.getLocalName(), provider);
114 }
115 }
116
117 protected QName toQName(Node source, String namespaceURI, String localName, NamespaceContext namespaceContext) {
118 return new ContentName(namespaceURI, localName, namespaceContext);
119 }
120
121 protected QName toQName(Node source, String localName) {
122 return new ContentName(localName);
123 }
124 /*
125 * ATTRIBUTES OPERATIONS
126 */
127
128 @Override
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
138 result.add(key);
139 }
140 return result;
141 }
142
143 @SuppressWarnings("unchecked")
144 @Override
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);
152 else
153 return Optional.empty();
154 } else
155 return Optional.empty();
156 }
157
158 @Override
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(),
166 value.toString());
167 return previous;
168 }
169
170 protected String registerPrefixIfNeeded(QName name) {
171 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
172 : name.getNamespaceURI();
173 String prefixToUse;
174 if (namespaceUriOrNull != null) {
175 String registeredPrefix = provider.getPrefix(namespaceUriOrNull);
176 if (registeredPrefix != null) {
177 prefixToUse = registeredPrefix;
178 } else {
179 provider.registerPrefix(name.getPrefix(), namespaceUriOrNull);
180 prefixToUse = name.getPrefix();
181 }
182 } else {
183 prefixToUse = null;
184 }
185 return prefixToUse;
186 }
187
188 @Override
189 public boolean hasText() {
190 // return element instanceof Text;
191 if (hasText != null)
192 return hasText;
193 NodeList nodeList = element.getChildNodes();
194 if (nodeList.getLength() > 1) {
195 hasText = false;
196 return hasText;
197 }
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()) {
203 hasText = true;
204 break nodes;
205 }
206 }
207 }
208 if (hasText == null)
209 hasText = false;
210 return hasText;
211 // if (text != null)
212 // return true;
213 // text = element.getTextContent();
214 // return text != null;
215 }
216
217 @Override
218 public String getText() {
219 if (hasText())
220 return element.getTextContent();
221 else
222 return null;
223 }
224
225 /*
226 * CONTENT OPERATIONS
227 */
228
229 @Override
230 public Iterator<Content> iterator() {
231 NodeList nodeList = element.getChildNodes();
232 return new ElementIterator(this, getSession(), provider, nodeList);
233 }
234
235 @Override
236 public Content getParent() {
237 Node parentNode = element.getParentNode();
238 if (isLocalRoot()) {
239 String mountPath = provider.getMountPath();
240 if (mountPath == null)
241 return null;
242 if (ContentUtils.ROOT_SLASH.equals(mountPath)) {
243 return null;
244 }
245 String[] parent = ContentUtils.getParentPath(mountPath);
246 if (ContentUtils.EMPTY.equals(parent[0]))
247 return null;
248 return getSession().get(parent[0]);
249 }
250 if (!(parentNode instanceof Element))
251 throw new IllegalStateException("Parent is not an element");
252 return new DomContent(this, (Element) parentNode);
253 }
254
255 @Override
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);
266 }
267
268 @Override
269 public void remove() {
270 // TODO make it more robust
271 element.getParentNode().removeChild(element);
272
273 }
274
275 @Override
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());
281
282 }
283
284 @SuppressWarnings("unchecked")
285 @Override
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);
290 return (A) buf;
291 } else if (Source.class.isAssignableFrom(clss)) {
292 DOMSource source = new DOMSource(element);
293 return (A) source;
294 }
295 return super.adapt(clss);
296 }
297
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);
305 });
306 return (CompletableFuture<A>) res;
307 } else if (Source.class.isAssignableFrom(clss)) {
308 CompletableFuture<Source> res = new CompletableFuture<>();
309 res.thenAccept((source) -> {
310 try {
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());
320
321 // attributes
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);
326 }
327
328 // Move all the children
329 while (element.hasChildNodes()) {
330 element.removeChild(element.getFirstChild());
331 }
332 while (resultElement.hasChildNodes()) {
333 element.appendChild(resultElement.getFirstChild());
334 }
335 // parentNode.replaceChild(resultNode, element);
336 // element = (Element)resultNode;
337
338 } catch (DOMException | TransformerException e) {
339 throw new RuntimeException("Cannot write to element", e);
340 }
341 });
342 return (CompletableFuture<A>) res;
343 }
344 return super.write(clss);
345 }
346
347 @SuppressWarnings("unchecked")
348 @Override
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(() -> {
353 try {
354 Source source = new DOMSource(element);
355 Result result = new StreamResult(out);
356 provider.getTransformerFactory().newTransformer().transform(source, result);
357 out.flush();
358 out.close();
359 } catch (TransformerException | IOException e) {
360 throw new RuntimeException("Cannot read " + getPath(), e);
361 }
362 });
363 return (C) new PipedInputStream(out);
364 }
365 return super.open(clss);
366 }
367
368 @Override
369 public int getSiblingIndex() {
370 Node curr = element.getPreviousSibling();
371 int count = 1;
372 while (curr != null) {
373 if (curr instanceof Element) {
374 if (Objects.equals(curr.getNamespaceURI(), element.getNamespaceURI())
375 && Objects.equals(curr.getLocalName(), element.getLocalName())) {
376 count++;
377 }
378 }
379 curr = curr.getPreviousSibling();
380 }
381 return count;
382 }
383
384 /*
385 * TYPING
386 */
387 @Override
388 public List<QName> getContentClasses() {
389 List<QName> res = new ArrayList<>();
390 if (isLocalRoot()) {
391 String mountPath = provider.getMountPath();
392 if (mountPath != null) {
393 Content mountPoint = getSession().getMountPoint(mountPath);
394 res.addAll(mountPoint.getContentClasses());
395 }
396 } else {
397 res.add(getName());
398 }
399 return res;
400 }
401
402 @Override
403 public void addContentClasses(QName... contentClass) {
404 if (isLocalRoot()) {
405 String mountPath = provider.getMountPath();
406 if (mountPath != null) {
407 Content mountPoint = getSession().getMountPoint(mountPath);
408 mountPoint.addContentClasses(contentClass);
409 }
410 } else {
411 super.addContentClasses(contentClass);
412 }
413 }
414
415 /*
416 * MOUNT MANAGEMENT
417 */
418 @Override
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);
424 }
425
426 @Override
427 public DomContentProvider getProvider() {
428 return provider;
429 }
430
431 }