Instance identifier in XML output was added
[yangtools.git] / yang / yang-data-impl / src / main / java / org / opendaylight / yangtools / yang / data / impl / codec / xml / XmlDocumentUtils.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.data.impl.codec.xml;
9
10 import static com.google.common.base.Preconditions.checkState;
11
12 import java.net.URI;
13 import java.util.Collection;
14 import java.util.HashMap;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Map.Entry;
18 import java.util.Random;
19 import java.util.Set;
20
21 import javax.activation.UnsupportedDataTypeException;
22 import javax.xml.parsers.DocumentBuilder;
23 import javax.xml.parsers.DocumentBuilderFactory;
24 import javax.xml.parsers.ParserConfigurationException;
25
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.data.api.AttributesContainer;
28 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
29 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifierWithPredicates;
31 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument;
32 import org.opendaylight.yangtools.yang.data.api.Node;
33 import org.opendaylight.yangtools.yang.data.api.SimpleNode;
34 import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode;
35 import org.opendaylight.yangtools.yang.data.impl.SimpleNodeTOImpl;
36 import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec;
37 import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
38 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
39 import org.opendaylight.yangtools.yang.model.api.ChoiceNode;
40 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
42 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
49 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52 import org.w3c.dom.Document;
53 import org.w3c.dom.Element;
54 import org.w3c.dom.NodeList;
55
56 import com.google.common.base.Function;
57 import com.google.common.base.Objects;
58 import com.google.common.base.Optional;
59 import com.google.common.base.Preconditions;
60 import com.google.common.base.Strings;
61 import com.google.common.collect.ImmutableList;
62
63 public class XmlDocumentUtils {
64
65     private static final XmlCodecProvider DEFAULT_XML_VALUE_CODEC_PROVIDER = new XmlCodecProvider() {
66
67         @Override
68         public TypeDefinitionAwareCodec<Object, ? extends TypeDefinition<?>> codecFor(TypeDefinition<?> baseType) {
69             return TypeDefinitionAwareCodec.from(baseType);
70         }
71     };
72
73     private static final Logger logger = LoggerFactory.getLogger(XmlDocumentUtils.class);
74
75     public static Document toDocument(CompositeNode data, DataNodeContainer schema, XmlCodecProvider codecProvider)
76             throws UnsupportedDataTypeException {
77         Preconditions.checkNotNull(data);
78         Preconditions.checkNotNull(schema);
79
80         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
81         Document doc = null;
82         try {
83             DocumentBuilder bob = dbf.newDocumentBuilder();
84             doc = bob.newDocument();
85         } catch (ParserConfigurationException e) {
86             return null;
87         }
88
89         if (schema instanceof ContainerSchemaNode || schema instanceof ListSchemaNode) {
90             doc.appendChild(createXmlRootElement(doc, data, (SchemaNode) schema, codecProvider));
91             return doc;
92         } else {
93             throw new UnsupportedDataTypeException(
94                     "Schema can be ContainerSchemaNode or ListSchemaNode. Other types are not supported yet.");
95         }
96     }
97
98     private static Element createXmlRootElement(Document doc, Node<?> data, SchemaNode schema,
99             XmlCodecProvider codecProvider) throws UnsupportedDataTypeException {
100         Element itemEl = createElementFor(doc, data);
101         if (data instanceof SimpleNode<?>) {
102             if (schema instanceof LeafListSchemaNode) {
103                 writeValueByType(itemEl, (SimpleNode<?>) data, ((LeafListSchemaNode) schema).getType(),
104                         (DataSchemaNode) schema, codecProvider);
105             } else if (schema instanceof LeafSchemaNode) {
106                 writeValueByType(itemEl, (SimpleNode<?>) data, ((LeafSchemaNode) schema).getType(),
107                         (DataSchemaNode) schema, codecProvider);
108             } else {
109                 Object value = data.getValue();
110                 if (value != null) {
111                     itemEl.setTextContent(String.valueOf(value));
112                 }
113             }
114         } else { // CompositeNode
115             for (Node<?> child : ((CompositeNode) data).getChildren()) {
116                 DataSchemaNode childSchema = null;
117                 if (schema != null) {
118                     childSchema = findFirstSchemaForNode(child, ((DataNodeContainer) schema).getChildNodes());
119                     if (logger.isDebugEnabled()) {
120                         if (childSchema == null) {
121                             logger.debug("Probably the data node \""
122                                     + ((child == null) ? "" : child.getNodeType().getLocalName())
123                                     + "\" is not conform to schema");
124                         }
125                     }
126                 }
127                 itemEl.appendChild(createXmlRootElement(doc, child, childSchema, codecProvider));
128             }
129         }
130         return itemEl;
131     }
132
133     private static Element createElementFor(Document doc, Node<?> data) {
134         QName dataType = data.getNodeType();
135         Element ret;
136         if (dataType.getNamespace() != null) {
137             ret = doc.createElementNS(dataType.getNamespace().toString(), dataType.getLocalName());
138         } else {
139             ret = doc.createElementNS(null, dataType.getLocalName());
140         }
141         if (data instanceof AttributesContainer && ((AttributesContainer) data).getAttributes() != null) {
142             for (Entry<QName, String> attribute : ((AttributesContainer) data).getAttributes().entrySet()) {
143                 ret.setAttributeNS(attribute.getKey().getNamespace().toString(), attribute.getKey().getLocalName(),
144                         attribute.getValue());
145             }
146
147         }
148         return ret;
149     }
150
151     public static void writeValueByType(Element element, SimpleNode<?> node, TypeDefinition<?> type,
152             DataSchemaNode schema, XmlCodecProvider codecProvider) {
153         TypeDefinition<?> baseType = resolveBaseTypeFrom(type);
154
155         if (baseType instanceof IdentityrefTypeDefinition) {
156             if (node.getValue() instanceof QName) {
157                 QName value = (QName) node.getValue();
158                 String prefix = "x";
159                 if (value.getPrefix() != null && !value.getPrefix().isEmpty()) {
160                     prefix = value.getPrefix();
161                 }
162                 element.setAttribute("xmlns:" + prefix, value.getNamespace().toString());
163                 element.setTextContent(prefix + ":" + value.getLocalName());
164             } else {
165                 Object value = node.getValue();
166                 logger.debug("Value of {}:{} is not instance of QName but is {}", baseType.getQName().getNamespace(),
167                         baseType.getQName().getLocalName(), value != null ? value.getClass() : "null");
168                 if (value != null) {
169                     element.setTextContent(String.valueOf(value));
170                 }
171             }
172         } else if (baseType instanceof InstanceIdentifierTypeDefinition) {
173             if (node.getValue() instanceof InstanceIdentifier) {
174                 // Map< key = namespace, value = prefix>
175                 Map<String, String> prefixes = new HashMap<>();
176                 InstanceIdentifier instanceIdentifier = (InstanceIdentifier) node.getValue();
177                 StringBuilder textContent = new StringBuilder();
178                 for (PathArgument pathArgument : instanceIdentifier.getPath()) {
179                     textContent.append("/");
180                     writeIdentifierWithNamespacePrefix(element, textContent, pathArgument.getNodeType(), prefixes);
181                     if (pathArgument instanceof NodeIdentifierWithPredicates) {
182                         Map<QName, Object> predicates = ((NodeIdentifierWithPredicates) pathArgument).getKeyValues();
183
184                         for (QName keyValue : predicates.keySet()) {
185                             String predicateValue = String.valueOf(predicates.get(keyValue));
186                             textContent.append("[");
187                             writeIdentifierWithNamespacePrefix(element, textContent, keyValue, prefixes);
188                             textContent.append("='");
189                             textContent.append(predicateValue);
190                             textContent.append("'");
191                             textContent.append("]");
192                         }
193                     }
194                 }
195                 element.setTextContent(textContent.toString());
196
197             } else {
198                 Object value = node.getValue();
199                 logger.debug("Value of {}:{} is not instance of InstanceIdentifier but is {}", baseType.getQName()
200                         .getNamespace(), //
201                         baseType.getQName().getLocalName(), value != null ? value.getClass() : "null");
202                 if (value != null) {
203                     element.setTextContent(String.valueOf(value));
204                 }
205             }
206         } else {
207             if (node.getValue() != null) {
208                 final TypeDefinitionAwareCodec<Object, ?> codec = codecProvider.codecFor(baseType);
209                 if (codec != null) {
210                     try {
211                         final String text = codec.serialize(node.getValue());
212                         element.setTextContent(text);
213                     } catch (ClassCastException e) {
214                         logger.error("Provided node did not have type required by mapping. Using stream instead.", e);
215                         element.setTextContent(String.valueOf(node.getValue()));
216                     }
217                 } else {
218                     logger.error("Failed to find codec for {}, falling back to using stream", baseType);
219                     element.setTextContent(String.valueOf(node.getValue()));
220                 }
221             }
222         }
223     }
224
225     private static void writeIdentifierWithNamespacePrefix(Element element, StringBuilder textContent, QName qName,
226             Map<String, String> prefixes) {
227         String namespace = qName.getNamespace().toString();
228         String prefix = prefixes.get(namespace);
229         if (prefix == null) {
230             prefix = qName.getPrefix();
231             if (prefix == null || prefix.isEmpty() || prefixes.containsValue(prefix)) {
232                 prefix = generateNewPrefix(prefixes.values());
233             }
234         }
235
236         element.setAttribute("xmlns:" + prefix, namespace.toString());
237         textContent.append(prefix);
238         prefixes.put(namespace, prefix);
239
240         textContent.append(":");
241         textContent.append(qName.getLocalName());
242     }
243
244     private static String generateNewPrefix(Collection<String> prefixes) {
245         StringBuilder result = null;
246         Random random = new Random();
247         do {
248             result = new StringBuilder();
249             for (int i = 0; i < 4; i++) {
250                 int randomNumber = 0x61 + (Math.abs(random.nextInt()) % 26);
251                 result.append(Character.toChars(randomNumber));
252             }
253         } while (prefixes.contains(result.toString()));
254
255         return result.toString();
256     }
257
258     public final static TypeDefinition<?> resolveBaseTypeFrom(TypeDefinition<?> type) {
259         TypeDefinition<?> superType = type;
260         while (superType.getBaseType() != null) {
261             superType = superType.getBaseType();
262         }
263         return superType;
264     }
265
266     private static final DataSchemaNode findFirstSchemaForNode(Node<?> node, Set<DataSchemaNode> dataSchemaNode) {
267         if (dataSchemaNode != null && node != null) {
268             for (DataSchemaNode dsn : dataSchemaNode) {
269                 if (node.getNodeType().getLocalName().equals(dsn.getQName().getLocalName())) {
270                     return dsn;
271                 } else if (dsn instanceof ChoiceNode) {
272                     for (ChoiceCaseNode choiceCase : ((ChoiceNode) dsn).getCases()) {
273                         DataSchemaNode foundDsn = findFirstSchemaForNode(node, choiceCase.getChildNodes());
274                         if (foundDsn != null) {
275                             return foundDsn;
276                         }
277                     }
278                 }
279             }
280         }
281         return null;
282     }
283
284     public static Node<?> toDomNode(Element xmlElement, Optional<DataSchemaNode> schema,
285             Optional<XmlCodecProvider> codecProvider) {
286         if (schema.isPresent()) {
287             return toNodeWithSchema(xmlElement, schema.get(), codecProvider.or(DEFAULT_XML_VALUE_CODEC_PROVIDER));
288         }
289         return toDomNode(xmlElement);
290     }
291
292     public static CompositeNode fromElement(Element xmlElement) {
293         CompositeNodeBuilder<ImmutableCompositeNode> node = ImmutableCompositeNode.builder();
294         node.setQName(qNameFromElement(xmlElement));
295
296         return node.toInstance();
297     }
298
299     private static QName qNameFromElement(Element xmlElement) {
300         String namespace = xmlElement.getNamespaceURI();
301         String localName = xmlElement.getLocalName();
302         return QName.create(namespace != null ? URI.create(namespace) : null, null, localName);
303     }
304
305     private static Node<?> toNodeWithSchema(Element xmlElement, DataSchemaNode schema, XmlCodecProvider codecProvider) {
306         checkQName(xmlElement, schema.getQName());
307         if (schema instanceof DataNodeContainer) {
308             return toCompositeNodeWithSchema(xmlElement, schema.getQName(), (DataNodeContainer) schema, codecProvider);
309         } else if (schema instanceof LeafSchemaNode) {
310             return toSimpleNodeWithType(xmlElement, (LeafSchemaNode) schema, codecProvider);
311         } else if (schema instanceof LeafListSchemaNode) {
312             return toSimpleNodeWithType(xmlElement, (LeafListSchemaNode) schema, codecProvider);
313
314         }
315         return null;
316     }
317
318     private static Node<?> toSimpleNodeWithType(Element xmlElement, LeafSchemaNode schema,
319             XmlCodecProvider codecProvider) {
320         TypeDefinitionAwareCodec<Object, ? extends TypeDefinition<?>> codec = codecProvider.codecFor(schema.getType());
321         String text = xmlElement.getTextContent();
322         Object value;
323         if (codec != null) {
324             value = codec.deserialize(text);
325
326         } else {
327             value = xmlElement.getTextContent();
328         }
329         return new SimpleNodeTOImpl<Object>(schema.getQName(), null, value);
330     }
331
332     private static Node<?> toSimpleNodeWithType(Element xmlElement, LeafListSchemaNode schema,
333             XmlCodecProvider codecProvider) {
334         TypeDefinitionAwareCodec<Object, ? extends TypeDefinition<?>> codec = codecProvider.codecFor(schema.getType());
335         String text = xmlElement.getTextContent();
336         Object value;
337         if (codec != null) {
338             value = codec.deserialize(text);
339
340         } else {
341             value = xmlElement.getTextContent();
342         }
343         return new SimpleNodeTOImpl<Object>(schema.getQName(), null, value);
344     }
345
346     private static Node<?> toCompositeNodeWithSchema(Element xmlElement, QName qName, DataNodeContainer schema,
347             XmlCodecProvider codecProvider) {
348         List<Node<?>> values = toDomNodes(xmlElement, Optional.fromNullable(schema.getChildNodes()));
349         return ImmutableCompositeNode.create(qName, values);
350     }
351
352     private static void checkQName(Element xmlElement, QName qName) {
353         checkState(Objects.equal(xmlElement.getNamespaceURI(), qName.getNamespace().toString()));
354         checkState(qName.getLocalName().equals(xmlElement.getLocalName()));
355     }
356
357     private static final Optional<DataSchemaNode> findFirstSchema(QName qname, Set<DataSchemaNode> dataSchemaNode) {
358         if (dataSchemaNode != null && !dataSchemaNode.isEmpty() && qname != null) {
359             for (DataSchemaNode dsn : dataSchemaNode) {
360                 if (qname.isEqualWithoutRevision(dsn.getQName())) {
361                     return Optional.<DataSchemaNode> of(dsn);
362                 } else if (dsn instanceof ChoiceNode) {
363                     for (ChoiceCaseNode choiceCase : ((ChoiceNode) dsn).getCases()) {
364                         Optional<DataSchemaNode> foundDsn = findFirstSchema(qname, choiceCase.getChildNodes());
365                         if (foundDsn != null) {
366                             return foundDsn;
367                         }
368                     }
369                 }
370             }
371         }
372         return Optional.absent();
373     }
374
375     public static Node<?> toDomNode(Document doc) {
376         return toDomNode(doc.getDocumentElement());
377     }
378
379     private static Node<?> toDomNode(Element element) {
380         QName qname = qNameFromElement(element);
381
382         ImmutableList.Builder<Node<?>> values = ImmutableList.<Node<?>> builder();
383         NodeList nodes = element.getChildNodes();
384         boolean isSimpleObject = true;
385         String value = null;
386         for (int i = 0; i < nodes.getLength(); i++) {
387             org.w3c.dom.Node child = nodes.item(i);
388             if (child instanceof Element) {
389                 isSimpleObject = false;
390                 values.add(toDomNode((Element) child));
391             }
392             if (isSimpleObject && child instanceof org.w3c.dom.Text) {
393                 value = element.getTextContent();
394                 if (!Strings.isNullOrEmpty(value)) {
395                     isSimpleObject = true;
396                 }
397             }
398         }
399         if (isSimpleObject) {
400             return new SimpleNodeTOImpl<>(qname, null, value);
401         }
402         return ImmutableCompositeNode.create(qname, values.build());
403     }
404
405     public static List<Node<?>> toDomNodes(final Element element, final Optional<Set<DataSchemaNode>> context) {
406         return forEachChild(element.getChildNodes(), new Function<Element, Optional<Node<?>>>() {
407
408             @Override
409             public Optional<Node<?>> apply(Element input) {
410                 if (context.isPresent()) {
411                     QName partialQName = qNameFromElement(input);
412                     Optional<DataSchemaNode> schemaNode = findFirstSchema(partialQName, context.get());
413                     if (schemaNode.isPresent()) {
414                         return Optional.<Node<?>> fromNullable(//
415                                 toNodeWithSchema(input, schemaNode.get(), DEFAULT_XML_VALUE_CODEC_PROVIDER));
416                     }
417                 }
418                 return Optional.<Node<?>> fromNullable(toDomNode(element));
419             }
420
421         });
422
423     }
424
425     private static final <T> List<T> forEachChild(NodeList nodes, Function<Element, Optional<T>> forBody) {
426         ImmutableList.Builder<T> ret = ImmutableList.<T> builder();
427         for (int i = 0; i < nodes.getLength(); i++) {
428             org.w3c.dom.Node child = nodes.item(i);
429             if (child instanceof Element) {
430                 Optional<T> result = forBody.apply((Element) child);
431                 if (result.isPresent()) {
432                     ret.add(result.get());
433                 }
434             }
435         }
436         return ret.build();
437     }
438
439     public static final XmlCodecProvider defaultValueCodecProvider() {
440         return DEFAULT_XML_VALUE_CODEC_PROVIDER;
441     }
442
443 }