]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java
Support writing file as XML
[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.Optional;
9 import java.util.Set;
10 import java.util.concurrent.CompletableFuture;
11
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;
20
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;
36
37 /** Content persisted as a DOM element. */
38 public class DomContent extends AbstractContent implements ProvidedContent {
39
40 private final DomContentProvider provider;
41 private final Element element;
42
43 // private String text = null;
44 private Boolean hasText = null;
45
46 public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) {
47 super(session);
48 this.provider = contentProvider;
49 this.element = element;
50 }
51
52 public DomContent(DomContent context, Element element) {
53 this(context.getSession(), context.getProvider(), element);
54 }
55
56 @Override
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();
63 }
64 }
65 return toQName(this.element);
66 }
67
68 protected QName toQName(Node node) {
69 String prefix = node.getPrefix();
70 if (prefix == null) {
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());
76 } else {
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);
81 }
82 } else {
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?
91 }
92 return toQName(node, namespaceURI, node.getLocalName(), provider);
93 }
94 }
95
96 protected QName toQName(Node source, String namespaceURI, String localName, NamespaceContext namespaceContext) {
97 return new ContentName(namespaceURI, localName, namespaceContext);
98 }
99
100 protected QName toQName(Node source, String localName) {
101 return new ContentName(localName);
102 }
103 /*
104 * ATTRIBUTES OPERATIONS
105 */
106
107 @Override
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);
115 result.add(key);
116 }
117 return result;
118 }
119
120 @SuppressWarnings("unchecked")
121 @Override
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);
129 else
130 return Optional.empty();
131 } else
132 return null;
133 }
134
135 @Override
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(),
143 value.toString());
144 return previous;
145 }
146
147 protected String registerPrefixIfNeeded(QName name) {
148 String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null
149 : name.getNamespaceURI();
150 String prefixToUse;
151 if (namespaceUriOrNull != null) {
152 String registeredPrefix = provider.getPrefix(namespaceUriOrNull);
153 if (registeredPrefix != null) {
154 prefixToUse = registeredPrefix;
155 } else {
156 provider.registerPrefix(name.getPrefix(), namespaceUriOrNull);
157 prefixToUse = name.getPrefix();
158 }
159 } else {
160 prefixToUse = null;
161 }
162 return prefixToUse;
163 }
164
165 @Override
166 public boolean hasText() {
167 // return element instanceof Text;
168 if (hasText != null)
169 return hasText;
170 NodeList nodeList = element.getChildNodes();
171 if (nodeList.getLength() > 1) {
172 hasText = false;
173 return hasText;
174 }
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()) {
180 hasText = true;
181 break nodes;
182 }
183 }
184 }
185 if (hasText == null)
186 hasText = false;
187 return hasText;
188 // if (text != null)
189 // return true;
190 // text = element.getTextContent();
191 // return text != null;
192 }
193
194 @Override
195 public String getText() {
196 if (hasText())
197 return element.getTextContent();
198 else
199 return null;
200 }
201
202 /*
203 * CONTENT OPERATIONS
204 */
205
206 @Override
207 public Iterator<Content> iterator() {
208 NodeList nodeList = element.getChildNodes();
209 return new ElementIterator(this, getSession(), provider, nodeList);
210 }
211
212 @Override
213 public Content getParent() {
214 Node parentNode = element.getParentNode();
215 if (parentNode == null) {
216 String mountPath = provider.getMountPath();
217 if (mountPath == null)
218 return null;
219 String[] parent = ContentUtils.getParentPath(mountPath);
220 return getSession().get(parent[0]);
221 }
222 if (parentNode instanceof Document)
223 return null;
224 if (!(parentNode instanceof Element))
225 throw new IllegalStateException("Parent is not an element");
226 return new DomContent(this, (Element) parentNode);
227 }
228
229 @Override
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);
240 }
241
242 @Override
243 public void remove() {
244 // TODO make it more robust
245 element.getParentNode().removeChild(element);
246
247 }
248
249 @Override
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());
255
256 }
257
258 @SuppressWarnings("unchecked")
259 @Override
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);
264 return (A) buf;
265 } else if (Source.class.isAssignableFrom(clss)) {
266 DOMSource source = new DOMSource(element);
267 return (A) source;
268 }
269 return super.adapt(clss);
270 }
271
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);
279 });
280 return (CompletableFuture<A>) res;
281 } else if (Source.class.isAssignableFrom(clss)) {
282 CompletableFuture<Source> res = new CompletableFuture<>();
283 res.thenAccept((source) -> {
284 try {
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());
294
295 // attributes
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);
300 }
301
302 // Move all the children
303 while (element.hasChildNodes()) {
304 element.removeChild(element.getFirstChild());
305 }
306 while (resultElement.hasChildNodes()) {
307 element.appendChild(resultElement.getFirstChild());
308 }
309 // parentNode.replaceChild(resultNode, element);
310 // element = (Element)resultNode;
311
312 } catch (DOMException | TransformerException e) {
313 throw new RuntimeException("Cannot write to element", e);
314 }
315 });
316 return (CompletableFuture<A>) res;
317 }
318 return super.write(clss);
319 }
320
321 /*
322 * TYPING
323 */
324 @Override
325 public List<QName> getContentClasses() {
326 List<QName> res = new ArrayList<>();
327 res.add(getName());
328 return res;
329 }
330
331 /*
332 * MOUNT MANAGEMENT
333 */
334 @Override
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);
340 }
341
342 @Override
343 public DomContentProvider getProvider() {
344 return provider;
345 }
346
347 }