Migrate netconf-client-mdsal/api tests to JUnit5
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / spi / NormalizedFormattableBody.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech, s.r.o. 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.server.spi;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.base.MoreObjects.ToStringHelper;
13 import com.google.common.base.VerifyException;
14 import com.google.gson.stream.JsonWriter;
15 import java.io.IOException;
16 import java.io.OutputStream;
17 import javax.xml.stream.XMLStreamException;
18 import javax.xml.stream.XMLStreamWriter;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.opendaylight.restconf.api.FormattableBody;
21 import org.opendaylight.restconf.api.query.PrettyPrintParam;
22 import org.opendaylight.restconf.server.api.DatabindContext;
23 import org.opendaylight.restconf.server.api.DatabindPath.Data;
24 import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
26 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
27 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
28 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
29 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
30 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
31 import org.opendaylight.yangtools.yang.data.codec.xml.XmlCodecFactory;
32 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
33
34 /**
35  * A {@link FormattableBody} representing a data resource.
36  */
37 @NonNullByDefault
38 public abstract sealed class NormalizedFormattableBody<N extends NormalizedNode> extends FormattableBody
39         permits DataFormattableBody, RootFormattableBody {
40     private final NormalizedNodeWriterFactory writerFactory;
41     private final DatabindContext databind;
42     private final N data;
43
44     NormalizedFormattableBody(final DatabindContext databind, final NormalizedNodeWriterFactory writerFactory,
45             final N data) {
46         this.databind = requireNonNull(databind);
47         this.writerFactory = requireNonNull(writerFactory);
48         this.data = requireNonNull(data);
49     }
50
51     public static NormalizedFormattableBody<?> of(final Data path, final NormalizedNode data,
52             final NormalizedNodeWriterFactory writerFactory) {
53         final var inference = path.inference();
54         if (inference.isEmpty()) {
55             // Read of the entire /data resource
56             if (data instanceof ContainerNode container) {
57                 return new RootFormattableBody(path.databind(), writerFactory, container);
58             }
59             throw new VerifyException("Unexpected root data contract " + data.contract());
60         }
61
62         // RESTCONF allows returning one list item. We need to wrap it in map node in order to serialize it properly.
63         // We need to point to a 'a parent inference' and provide an appropriate data entry. Unfortunately it is not
64         // quite defined what that actually means.
65         //
66         // This is a tricky thing, as JSON and XML have different representations of a MapNode. In JSON it is the array
67         // containing individual objects. In XML it is transparent.
68         //
69         // This means that for XML we could just move 'parent inference' to the MapNode and emit the MapEntryNode as
70         // usual. For JSON that does not work, as we also need to wrap the MapEntryNode in a MapNode.
71         //
72         // What we do here is we unconditionally:
73         //   - wrap the node if it is a list entry node
74         //   - move the inference to parent
75         //
76         // For XML that does not seem to matter. For JSON it does matter a lot.
77         final var stack = inference.toSchemaInferenceStack();
78         stack.exit();
79
80         return new DataFormattableBody<>(path.databind(), stack.toInference(), data instanceof MapEntryNode mapEntry
81             ? ImmutableNodes.newSystemMapBuilder()
82                 .withNodeIdentifier(new NodeIdentifier(data.name().getNodeType()))
83                 .withChild(mapEntry)
84                 .build()
85             : data, writerFactory);
86     }
87
88     /**
89      * Return a {@link FormattableBody} corresponding to a {@code rpc} or {@code action} invocation.
90      *
91      * @param path invocation path
92      * @param data the data
93      */
94     public static NormalizedFormattableBody<ContainerNode> of(final OperationPath path,
95             final ContainerNode data) {
96         return new DataFormattableBody<>(path.databind(), path.inference(), data);
97     }
98
99     /**
100      * Return data.
101      *
102      * @return data
103      */
104     public final N data() {
105         return data;
106     }
107
108     @Override
109     public final void formatToJSON(final PrettyPrintParam prettyPrint, final OutputStream out) throws IOException {
110         try (var writer = FormattableBodySupport.createJsonWriter(out, prettyPrint)) {
111             formatToJSON(databind.jsonCodecs(), data, writer);
112         }
113     }
114
115     protected abstract void formatToJSON(JSONCodecFactory codecs, N data, JsonWriter writer) throws IOException;
116
117     @Override
118     public final void formatToXML(final PrettyPrintParam prettyPrint, final OutputStream out) throws IOException {
119         final var writer = FormattableBodySupport.createXmlWriter(out, prettyPrint);
120         try {
121             formatToXML(databind.xmlCodecs(), data, writer);
122             writer.close();
123         } catch (XMLStreamException e) {
124             throw new IOException("Failed to write data", e);
125         }
126     }
127
128     protected abstract void formatToXML(XmlCodecFactory codecs, N data, XMLStreamWriter writer)
129         throws IOException, XMLStreamException;
130
131     protected final NormalizedNodeWriter newWriter(final NormalizedNodeStreamWriter streamWriter) {
132         return writerFactory.newWriter(streamWriter);
133     }
134
135     final void writeTo(final NormalizedNode toWrite, final NormalizedNodeStreamWriter streamWriter)
136             throws IOException {
137         try (var writer = newWriter(streamWriter)) {
138             writer.write(toWrite);
139         }
140     }
141
142     @Override
143     protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
144         return helper.add("body", data.prettyTree());
145     }
146 }