Fix retrieving operations resource
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / services / impl / OperationsContent.java
1 /*
2  * Copyright (c) 2021 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.nb.rfc8040.rests.services.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Map.Entry;
15 import org.eclipse.jdt.annotation.NonNull;
16 import org.eclipse.jdt.annotation.Nullable;
17 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
18 import org.opendaylight.yangtools.yang.common.QNameModule;
19 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
20 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
21 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
22
23 /**
24  * RESTCONF {@code /operations} content for a {@code GET} operation as per
25  * <a href="https://datatracker.ietf.org/doc/html/rfc8040#section-3.3.2">RFC8040</a>.
26  */
27 enum OperationsContent {
28     JSON("{ \"ietf-restconf:operations\" : { } }") {
29         @Override
30         String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
31             final var sb = new StringBuilder("{\n"
32                 + "  \"ietf-restconf:operations\" : {\n");
33             var entryIt = rpcsByPrefix.iterator();
34             var entry = entryIt.next();
35             var nameIt = entry.getValue().iterator();
36             while (true) {
37                 sb.append("    \"").append(entry.getKey()).append(':').append(nameIt.next()).append("\": [null]");
38                 if (nameIt.hasNext()) {
39                     sb.append(",\n");
40                     continue;
41                 }
42
43                 if (entryIt.hasNext()) {
44                     sb.append(",\n");
45                     entry = entryIt.next();
46                     nameIt = entry.getValue().iterator();
47                     continue;
48                 }
49
50                 break;
51             }
52
53             return sb.append("\n  }\n}").toString();
54         }
55
56         @Override
57         String prefix(final ModuleEffectiveStatement module) {
58             return module.argument().getLocalName();
59         }
60     },
61
62     XML("<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"/>") {
63         @Override
64         String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
65             // Header with namespace declarations for each module
66             final var sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
67                 + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"");
68             for (int i = 0; i < rpcsByPrefix.size(); ++i) {
69                 final var prefix = "ns" + i;
70                 sb.append("\n            xmlns:").append(prefix).append("=\"").append(rpcsByPrefix.get(i).getKey())
71                     .append("\"");
72             }
73             sb.append(" >");
74
75             // Second pass: emit all leaves
76             for (int i = 0; i < rpcsByPrefix.size(); ++i) {
77                 final var prefix = "ns" + i;
78                 for (var localName : rpcsByPrefix.get(i).getValue()) {
79                     sb.append("\n  <").append(prefix).append(':').append(localName).append("/>");
80                 }
81             }
82
83             return sb.append("\n</operations>").toString();
84         }
85
86         @Override
87         String prefix(final ModuleEffectiveStatement module) {
88             return module.localQNameModule().getNamespace().toString();
89         }
90     };
91
92     private final @NonNull String emptyBody;
93
94     OperationsContent(final String emptyBody) {
95         this.emptyBody = requireNonNull(emptyBody);
96     }
97
98     /**
99      * Return the content for a particular {@link EffectiveModelContext}.
100      *
101      * @param context Context to use
102      * @return Content of HTTP GET operation as a String
103      */
104     public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
105         if (isEmptyContext(context)) {
106             // No modules, or defensive return empty content
107             return emptyBody;
108         }
109
110         final var moduleRpcs = getModuleRpcs(context, context.getModuleStatements());
111         return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
112     }
113
114     /**
115      * Return content with RPCs and actions for a particular {@link InstanceIdentifierContext}.
116      *
117      * @param identifierContext InstanceIdentifierContext to use
118      * @return Content of HTTP GET operation as a String
119      */
120     public final @NonNull String bodyFor(final @NonNull InstanceIdentifierContext identifierContext) {
121         final var context = identifierContext.getSchemaContext();
122         if (isEmptyContext(context)) {
123             // No modules, or defensive return empty content
124             return emptyBody;
125         }
126
127         final var stack = identifierContext.inference().toSchemaInferenceStack();
128         // empty stack == get all RPCs/actions
129         if (stack.isEmpty()) {
130             return createBody(getModuleRpcs(context, context.getModuleStatements()));
131         }
132
133         // get current module RPCs/actions by RPC/action name
134         final var currentModule = stack.currentModule();
135         final var currentModuleKey = Map.of(currentModule.localQNameModule(), currentModule);
136         final var rpcName = identifierContext.getSchemaNode().getQName().getLocalName();
137         return getModuleRpcs(context, currentModuleKey).stream()
138             .findFirst()
139             .map(e -> Map.entry(e.getKey(), e.getValue().stream().filter(rpcName::equals).toList()))
140             .map(e -> createBody(List.of(e)))
141             .orElse(emptyBody);
142     }
143
144     private static boolean isEmptyContext(final EffectiveModelContext context) {
145         if (context == null) {
146             return true;
147         }
148         return context.getModuleStatements().isEmpty();
149     }
150
151     /**
152      * Returns a list of entries, where each entry contains a module prefix and a list of RPC names.
153      *
154      * @param context the effective model context
155      * @param modules the map of QNameModule to ModuleEffectiveStatement
156      * @return a list of entries, where each entry contains a module prefix and a list of RPC names
157      */
158     private List<Entry<@NonNull String, List<String>>> getModuleRpcs(final EffectiveModelContext context,
159             final Map<QNameModule, ModuleEffectiveStatement> modules) {
160         return modules.values().stream()
161                 // Extract XMLNamespaces
162                 .map(module -> module.localQNameModule().getNamespace())
163                 // Make sure each is XMLNamespace unique
164                 .distinct()
165                 // Find the most recent module with that namespace. This needed so we expose the right set of RPCs,
166                 // as we always pick the latest revision to resolve prefix (or module name).
167                 .map(namespace -> context.findModuleStatements(namespace).iterator().next())
168                 // Convert to module prefix + List<String> with RPC names
169                 .map(module -> Map.entry(prefix(module),
170                         module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
171                         .map(rpc -> rpc.argument().getLocalName())
172                         .toList()))
173                 // Skip prefixes which do not have any RPCs
174                 .filter(entry -> !entry.getValue().isEmpty())
175                 // Ensure stability: sort by prefix
176                 .sorted(Entry.comparingByKey())
177                 .toList();
178     }
179
180     abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
181
182     abstract @NonNull String prefix(ModuleEffectiveStatement module);
183 }