]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java
HTTP parameters support
[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.Closeable;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.PipedInputStream;
7 import java.io.PipedOutputStream;
8 import java.nio.CharBuffer;
9 import java.util.ArrayList;
10 import java.util.HashSet;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.Objects;
14 import java.util.Optional;
15 import java.util.Set;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.ForkJoinPool;
18
19 import javax.xml.XMLConstants;
20 import javax.xml.namespace.NamespaceContext;
21 import javax.xml.namespace.QName;
22 import javax.xml.transform.Result;
23 import javax.xml.transform.Source;
24 import javax.xml.transform.Transformer;
25 import javax.xml.transform.TransformerException;
26 import javax.xml.transform.dom.DOMResult;
27 import javax.xml.transform.dom.DOMSource;
28 import javax.xml.transform.stream.StreamResult;
29
30 import org.argeo.api.acr.Content;
31 import org.argeo.api.acr.ContentName;
32 import org.argeo.api.acr.CrAttributeType;
33 import org.argeo.api.acr.CrName;
34 import org.argeo.api.acr.spi.ProvidedContent;
35 import org.argeo.api.acr.spi.ProvidedSession;
36 import org.argeo.cms.acr.AbstractContent;
37 import org.argeo.cms.acr.ContentUtils;
38 import org.w3c.dom.Attr;
39 import org.w3c.dom.DOMException;
40 import org.w3c.dom.Document;
41 import org.w3c.dom.DocumentFragment;
42 import org.w3c.dom.Element;
43 import org.w3c.dom.NamedNodeMap;
44 import org.w3c.dom.Node;
45 import org.w3c.dom.NodeList;
46 import org.w3c.dom.Text;
47
48 /** Content persisted as a DOM element. */
49 public class DomContent extends AbstractContent implements ProvidedContent {
50
51 private final DomContentProvider provider;
52 private final Element element;
53
54 // private String text = null;
55 private Boolean hasText = null;
56
57 public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) {
58 super(session);
59 this.provider = contentProvider;
60 this.element = element;
61 }
62
63 public DomContent(DomContent context, Element element) {
64 this(context.getSession(), context.getProvider(), element);
65 }
66
67 @Override
68 public QName getName() {
69 if (isLocalRoot()) {// root
70 String mountPath = provider.getMountPath();
71 if (mountPath != null) {
72 if (Content.ROOT_PATH.equals(mountPath)) {
73 return CrName.root.qName();
74 }
75 Content mountPoint = getSession().getMountPoint(mountPath);
76 QName mountPointName = mountPoint.getName();
77 return mountPointName;
78 }
79 }
80 return toQName(this.element);
81 }
82
83 protected boolean isLocalRoot() {
84 return element.getParentNode() == null || element.getParentNode() instanceof Document;
85 }
86
87 protected QName toQName(Node node) {
88 String prefix = node.getPrefix();
89 if (prefix == null) {
90 String namespaceURI = node.getNamespaceURI();
91 if (namespaceURI == null)
92 namespaceURI = node.getOwnerDocument().lookupNamespaceURI(null);
93 if (namespaceURI == null) {
94 return toQName(node, node.getLocalName());
95 } else {
96 String contextPrefix = provider.getPrefix(namespaceURI);
97 if (contextPrefix == null)
98 throw new IllegalStateException("Namespace " + namespaceURI + " is unbound");
99 return toQName(node, namespaceURI, node.getLocalName(), provider);
100 }
101 } else {
102 String namespaceURI = node.getNamespaceURI();
103 if (namespaceURI == null)
104 namespaceURI = node.getOwnerDocument().lookupNamespaceURI(prefix);
105 if (namespaceURI == null) {
106 namespaceURI = provider.getNamespaceURI(prefix);
107 if (XMLConstants.NULL_NS_URI.equals(namespaceURI))
108 throw new IllegalStateException("Prefix " + prefix + " is unbound");
109 // TODO bind the prefix in the document?
110 }
111 return toQName(node, namespaceURI, node.getLocalName(), provider);
112 }
113 }
114
115 protected QName toQName(Node source, String namespaceURI, String localName, NamespaceContext namespaceContext) {
116 return new ContentName(namespaceURI, localName, namespaceContext);
117 }
118
119 protected QName toQName(Node source, String localName) {
120 return new ContentName(localName);
121 }
122 /*
123 * ATTRIBUTES OPERATIONS
124 */
125
126 @Override
127 public Iterable<QName> keys() {
128 // TODO implement an iterator?
129 Set<QName> result = new HashSet<>();
130 NamedNodeMap attributes = element.getAttributes();
131 for (int i = 0; i < attributes.getLength(); i++) {
132 Attr attr = (Attr) attributes.item(i);
133 QName key = toQName(attr);
134 if (key.getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI))
135 continue;// skip prefix mapping
136 result.add(key);
137 }
138 return result;
139 }
140
141 // @SuppressWarnings("unchecked")
142 @Override
143 public <A> Optional<A> get(QName key, Class<A> clss) {
144 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null
145 : key.getNamespaceURI();
146 if (element.hasAttributeNS(namespaceUriOrNull, key.getLocalPart())) {
147 String value = element.getAttributeNS(namespaceUriOrNull, key.getLocalPart());
148 // if (isDefaultAttrTypeRequested(clss))
149 // return Optional.of((A) CrAttributeType.parse(value));
150 return CrAttributeType.cast(clss, value);
151 } else
152 return Optional.empty();
153 }
154
155 @Override
156 public Object put(QName key, Object value) {
157 Object previous = get(key);
158 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null
159 : key.getNamespaceURI();
160 String prefixToUse = registerPrefixIfNeeded(key);
161 element.setAttributeNS(namespaceUriOrNull,
162 namespaceUriOrNull == null ? key.getLocalPart() : prefixToUse + ":" + key.getLocalPart(),
163 value.toString());
164 return previous;
165 }
166
167 protected String registerPrefixIfNeeded(QName name) {
168 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
169 : name.getNamespaceURI();
170 String prefixToUse;
171 if (namespaceUriOrNull != null) {
172 String registeredPrefix = provider.getPrefix(namespaceUriOrNull);
173 if (registeredPrefix != null) {
174 prefixToUse = registeredPrefix;
175 } else {
176 provider.registerPrefix(name.getPrefix(), namespaceUriOrNull);
177 prefixToUse = name.getPrefix();
178 }
179 } else {
180 prefixToUse = null;
181 }
182 return prefixToUse;
183 }
184
185 @Override
186 public boolean hasText() {
187 // return element instanceof Text;
188 if (hasText != null)
189 return hasText;
190 NodeList nodeList = element.getChildNodes();
191 if (nodeList.getLength() > 1) {
192 hasText = false;
193 return hasText;
194 }
195 nodes: for (int i = 0; i < nodeList.getLength(); i++) {
196 Node node = nodeList.item(i);
197 if (node instanceof Text) {
198 Text text = (Text) node;
199 if (!text.isElementContentWhitespace()) {
200 hasText = true;
201 break nodes;
202 }
203 }
204 }
205 if (hasText == null)
206 hasText = false;
207 return hasText;
208 // if (text != null)
209 // return true;
210 // text = element.getTextContent();
211 // return text != null;
212 }
213
214 @Override
215 public String getText() {
216 if (hasText())
217 return element.getTextContent();
218 else
219 return null;
220 }
221
222 /*
223 * CONTENT OPERATIONS
224 */
225
226 @Override
227 public Iterator<Content> iterator() {
228 NodeList nodeList = element.getChildNodes();
229 return new ElementIterator(this, getSession(), provider, nodeList);
230 }
231
232 @Override
233 public Content getParent() {
234 Node parentNode = element.getParentNode();
235 if (isLocalRoot()) {
236 String mountPath = provider.getMountPath();
237 if (mountPath == null)
238 return null;
239 if (Content.ROOT_PATH.equals(mountPath)) {
240 return null;
241 }
242 String[] parent = ContentUtils.getParentPath(mountPath);
243 if (ContentUtils.EMPTY.equals(parent[0]))
244 return null;
245 return getSession().get(parent[0]);
246 }
247 if (!(parentNode instanceof Element))
248 throw new IllegalStateException("Parent is not an element");
249 return new DomContent(this, (Element) parentNode);
250 }
251
252 @Override
253 public Content add(QName name, QName... classes) {
254 // TODO consider classes
255 Document document = this.element.getOwnerDocument();
256 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
257 : name.getNamespaceURI();
258 String prefixToUse = registerPrefixIfNeeded(name);
259 Element child = document.createElementNS(namespaceUriOrNull,
260 namespaceUriOrNull == null ? name.getLocalPart() : prefixToUse + ":" + name.getLocalPart());
261 element.appendChild(child);
262 return new DomContent(this, child);
263 }
264
265 @Override
266 public void remove() {
267 // TODO make it more robust
268 element.getParentNode().removeChild(element);
269
270 }
271
272 @Override
273 protected void removeAttr(QName key) {
274 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null
275 : key.getNamespaceURI();
276 element.removeAttributeNS(namespaceUriOrNull,
277 namespaceUriOrNull == null ? key.getLocalPart() : key.getPrefix() + ":" + key.getLocalPart());
278
279 }
280
281 @SuppressWarnings("unchecked")
282 @Override
283 public <A> A adapt(Class<A> clss) throws IllegalArgumentException {
284 if (CharBuffer.class.isAssignableFrom(clss)) {
285 String textContent = element.getTextContent();
286 CharBuffer buf = CharBuffer.wrap(textContent);
287 return (A) buf;
288 } else if (Source.class.isAssignableFrom(clss)) {
289 DOMSource source = new DOMSource(element);
290 return (A) source;
291 }
292 return super.adapt(clss);
293 }
294
295 @SuppressWarnings("unchecked")
296 public <A> CompletableFuture<A> write(Class<A> clss) {
297 if (String.class.isAssignableFrom(clss)) {
298 CompletableFuture<String> res = new CompletableFuture<>();
299 res.thenAccept((s) -> {
300 getSession().notifyModification(this);
301 element.setTextContent(s);
302 });
303 return (CompletableFuture<A>) res;
304 } else if (Source.class.isAssignableFrom(clss)) {
305 CompletableFuture<Source> res = new CompletableFuture<>();
306 res.thenAccept((source) -> {
307 try {
308 Transformer transformer = provider.getTransformerFactory().newTransformer();
309 DocumentFragment documentFragment = element.getOwnerDocument().createDocumentFragment();
310 DOMResult result = new DOMResult(documentFragment);
311 transformer.transform(source, result);
312 // Node parentNode = element.getParentNode();
313 Element resultElement = (Element) documentFragment.getFirstChild();
314 QName resultName = toQName(resultElement);
315 if (!resultName.equals(getName()))
316 throw new IllegalArgumentException(resultName + "+ is not compatible with " + getName());
317
318 // attributes
319 NamedNodeMap attrs = resultElement.getAttributes();
320 for (int i = 0; i < attrs.getLength(); i++) {
321 Attr attr2 = (Attr) element.getOwnerDocument().importNode(attrs.item(i), true);
322 element.getAttributes().setNamedItem(attr2);
323 }
324
325 // Move all the children
326 while (element.hasChildNodes()) {
327 element.removeChild(element.getFirstChild());
328 }
329 while (resultElement.hasChildNodes()) {
330 element.appendChild(resultElement.getFirstChild());
331 }
332 // parentNode.replaceChild(resultNode, element);
333 // element = (Element)resultNode;
334
335 } catch (DOMException | TransformerException e) {
336 throw new RuntimeException("Cannot write to element", e);
337 }
338 });
339 return (CompletableFuture<A>) res;
340 }
341 return super.write(clss);
342 }
343
344 @SuppressWarnings("unchecked")
345 @Override
346 public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
347 if (InputStream.class.isAssignableFrom(clss)) {
348 PipedOutputStream out = new PipedOutputStream();
349 ForkJoinPool.commonPool().execute(() -> {
350 try {
351 Source source = new DOMSource(element);
352 Result result = new StreamResult(out);
353 provider.getTransformerFactory().newTransformer().transform(source, result);
354 out.flush();
355 out.close();
356 } catch (TransformerException | IOException e) {
357 throw new RuntimeException("Cannot read " + getPath(), e);
358 }
359 });
360 return (C) new PipedInputStream(out);
361 }
362 return super.open(clss);
363 }
364
365 @Override
366 public int getSiblingIndex() {
367 Node curr = element.getPreviousSibling();
368 int count = 1;
369 while (curr != null) {
370 if (curr instanceof Element) {
371 if (Objects.equals(curr.getNamespaceURI(), element.getNamespaceURI())
372 && Objects.equals(curr.getLocalName(), element.getLocalName())) {
373 count++;
374 }
375 }
376 curr = curr.getPreviousSibling();
377 }
378 return count;
379 }
380
381 /*
382 * TYPING
383 */
384 @Override
385 public List<QName> getContentClasses() {
386 List<QName> res = new ArrayList<>();
387 if (isLocalRoot()) {
388 String mountPath = provider.getMountPath();
389 if (Content.ROOT_PATH.equals(mountPath)) {// repository root
390 res.add(CrName.root.qName());
391 } else {
392 Content mountPoint = getSession().getMountPoint(mountPath);
393 res.addAll(mountPoint.getContentClasses());
394 }
395 } else {
396 res.add(getName());
397 }
398 return res;
399 }
400
401 @Override
402 public void addContentClasses(QName... contentClass) {
403 if (isLocalRoot()) {
404 String mountPath = provider.getMountPath();
405 if (Content.ROOT_PATH.equals(mountPath)) {// repository root
406 throw new IllegalArgumentException("Cannot add content classes to repository root");
407 } else {
408 Content mountPoint = getSession().getMountPoint(mountPath);
409 mountPoint.addContentClasses(contentClass);
410 }
411 } else {
412 super.addContentClasses(contentClass);
413 }
414 }
415
416 /*
417 * MOUNT MANAGEMENT
418 */
419 @Override
420 public ProvidedContent getMountPoint(String relativePath) {
421 // FIXME use qualified names
422 Element childElement = (Element) element.getElementsByTagName(relativePath).item(0);
423 // TODO check that it is a mount
424 return new DomContent(this, childElement);
425 }
426
427 @Override
428 public DomContentProvider getProvider() {
429 return provider;
430 }
431
432 }