Clean up *NormalizedNodeBody{Reader,Writer}
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / jersey / providers / XmlNormalizedNodeBodyWriter.java
1 /*
2  * Copyright (c) 2016 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.restconf.nb.rfc8040.jersey.providers;
9
10 import java.io.IOException;
11 import java.io.OutputStream;
12 import java.lang.annotation.Annotation;
13 import java.lang.reflect.Type;
14 import java.nio.charset.StandardCharsets;
15 import java.util.List;
16 import java.util.Set;
17 import javanet.staxutils.IndentingXMLStreamWriter;
18 import javax.ws.rs.Produces;
19 import javax.ws.rs.WebApplicationException;
20 import javax.ws.rs.core.MediaType;
21 import javax.ws.rs.core.MultivaluedMap;
22 import javax.ws.rs.ext.Provider;
23 import javax.xml.XMLConstants;
24 import javax.xml.stream.FactoryConfigurationError;
25 import javax.xml.stream.XMLOutputFactory;
26 import javax.xml.stream.XMLStreamException;
27 import javax.xml.stream.XMLStreamWriter;
28 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
29 import org.opendaylight.restconf.nb.rfc8040.DepthParam;
30 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
31 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
32 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
33 import org.opendaylight.yangtools.yang.common.QName;
34 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
35 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
36 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
37 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
38 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
39 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
40 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
41 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
42 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
43 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
44
45 @Provider
46 @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
47 public class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBodyWriter {
48     private static final XMLOutputFactory XML_FACTORY;
49
50     static {
51         XML_FACTORY = XMLOutputFactory.newFactory();
52         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
53     }
54
55     @Override
56     public void writeTo(final NormalizedNodePayload context,
57                         final Class<?> type,
58                         final Type genericType,
59                         final Annotation[] annotations,
60                         final MediaType mediaType,
61                         final MultivaluedMap<String, Object> httpHeaders,
62                         final OutputStream entityStream) throws IOException, WebApplicationException {
63         final InstanceIdentifierContext pathContext = context.getInstanceIdentifierContext();
64         if (context.getData() == null) {
65             return;
66         }
67         if (httpHeaders != null) {
68             for (var entry : context.getNewHeaders().entrySet()) {
69                 httpHeaders.add(entry.getKey(), entry.getValue());
70             }
71         }
72
73         XMLStreamWriter xmlWriter;
74         try {
75             xmlWriter = XML_FACTORY.createXMLStreamWriter(entityStream, StandardCharsets.UTF_8.name());
76
77             final var prettyPrint = context.getWriterParameters().prettyPrint();
78             if (prettyPrint != null && prettyPrint.value()) {
79                 xmlWriter = new IndentingXMLStreamWriter(xmlWriter);
80             }
81         } catch (final XMLStreamException | FactoryConfigurationError e) {
82             throw new IllegalStateException(e);
83         }
84         final NormalizedNode data = context.getData();
85
86         writeNormalizedNode(xmlWriter,  pathContext, data, context.getWriterParameters().depth(),
87                 context.getWriterParameters().fields());
88     }
89
90     private static void writeNormalizedNode(final XMLStreamWriter xmlWriter,
91             final InstanceIdentifierContext pathContext, final NormalizedNode data, final DepthParam depth,
92             final List<Set<QName>> fields) throws IOException {
93         final RestconfNormalizedNodeWriter nnWriter;
94         final SchemaNode schemaNode = pathContext.getSchemaNode();
95
96         if (schemaNode instanceof RpcDefinition rpc) {
97             // RpcDefinition is not supported as initial codec in XMLStreamWriter, so we need to emit initial output
98             // declaration.
99             final var stack = SchemaInferenceStack.of(pathContext.getSchemaContext());
100             stack.enterSchemaTree(rpc.getQName());
101             stack.enterSchemaTree(rpc.getOutput().getQName());
102
103             nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), depth, fields);
104             writeElements(xmlWriter, nnWriter, (ContainerNode) data);
105         } else if (schemaNode instanceof ActionDefinition action) {
106             // ActionDefinition is not supported as initial codec in XMLStreamWriter, so we need to emit initial output
107             // declaration.
108             final var stack = pathContext.inference().toSchemaInferenceStack();
109             stack.enterSchemaTree(action.getOutput().getQName());
110
111             nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), depth, fields);
112             writeElements(xmlWriter, nnWriter, (ContainerNode) data);
113         } else {
114             final var stack = pathContext.inference().toSchemaInferenceStack();
115             final boolean isRoot;
116             if (!stack.isEmpty()) {
117                 stack.exit();
118                 isRoot = false;
119             } else {
120                 isRoot = true;
121             }
122             nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), depth, fields);
123
124             if (data instanceof MapEntryNode mapEntry) {
125                 // Restconf allows returning one list item. We need to wrap it
126                 // in map node in order to serialize it properly
127                 nnWriter.write(ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
128                     .addChild(mapEntry)
129                     .build());
130             } else if (isRoot) {
131                 if (data instanceof ContainerNode container && container.isEmpty()) {
132                     writeEmptyDataNode(xmlWriter, container);
133                 } else {
134                     writeAndWrapInDataNode(xmlWriter, nnWriter, data);
135                 }
136             } else {
137                 nnWriter.write(data);
138             }
139         }
140
141         nnWriter.flush();
142     }
143
144     private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final XMLStreamWriter xmlWriter,
145             final Inference inference, final DepthParam depth,
146             final List<Set<QName>> fields) {
147         return ParameterAwareNormalizedNodeWriter.forStreamWriter(
148             XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, inference), depth, fields);
149     }
150
151     private static void writeAndWrapInDataNode(final XMLStreamWriter xmlWriter,
152             final RestconfNormalizedNodeWriter nnWriter, final NormalizedNode data) throws IOException {
153         final QName nodeType = data.getIdentifier().getNodeType();
154         final String namespace = nodeType.getNamespace().toString();
155         try {
156             xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace);
157             xmlWriter.writeDefaultNamespace(namespace);
158             nnWriter.write(data);
159             xmlWriter.writeEndElement();
160             xmlWriter.flush();
161         } catch (XMLStreamException e) {
162             throw new IOException("Failed to write elements", e);
163         }
164     }
165
166     private static void writeEmptyDataNode(final XMLStreamWriter xmlWriter, final ContainerNode data)
167             throws IOException {
168         final QName nodeType = data.getIdentifier().getNodeType();
169         final String namespace = nodeType.getNamespace().toString();
170         try {
171             xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace);
172             xmlWriter.writeDefaultNamespace(namespace);
173             xmlWriter.writeEndElement();
174             xmlWriter.flush();
175         } catch (XMLStreamException e) {
176             throw new IOException("Failed to write elements", e);
177         }
178     }
179
180     private static void writeElements(final XMLStreamWriter xmlWriter, final RestconfNormalizedNodeWriter nnWriter,
181             final ContainerNode data) throws IOException {
182         final QName nodeType = data.getIdentifier().getNodeType();
183         final String namespace = nodeType.getNamespace().toString();
184         try {
185             xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace);
186             xmlWriter.writeDefaultNamespace(namespace);
187             for (var child : data.body()) {
188                 nnWriter.write(child);
189             }
190             nnWriter.flush();
191             xmlWriter.writeEndElement();
192             xmlWriter.flush();
193         } catch (final XMLStreamException e) {
194             throw new IOException("Failed to write elements", e);
195         }
196     }
197 }