Simplify OperationsContext with yangtools 7.0.9+
[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.yangtools.yang.model.api.EffectiveModelContext;
18 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
19 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
20
21 /**
22  * RESTCONF {@code /operations} content for a {@code GET} operation as per
23  * <a href="https://datatracker.ietf.org/doc/html/rfc8040#section-3.3.2">RFC8040</a>.
24  */
25 enum OperationsContent {
26     JSON("{ \"ietf-restconf:operations\" : { } }") {
27         @Override
28         String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
29             final var sb = new StringBuilder("{\n"
30                 + "  \"ietf-restconf:operations\" : {\n");
31             var entryIt = rpcsByPrefix.iterator();
32             var entry = entryIt.next();
33             var nameIt = entry.getValue().iterator();
34             while (true) {
35                 sb.append("    \"").append(entry.getKey()).append(':').append(nameIt.next()).append("\": [null]");
36                 if (nameIt.hasNext()) {
37                     sb.append(",\n");
38                     continue;
39                 }
40
41                 if (entryIt.hasNext()) {
42                     sb.append(",\n");
43                     entry = entryIt.next();
44                     nameIt = entry.getValue().iterator();
45                     continue;
46                 }
47
48                 break;
49             }
50
51             return sb.append("\n  }\n}").toString();
52         }
53
54         @Override
55         String prefix(final ModuleEffectiveStatement module) {
56             return module.argument().getLocalName();
57         }
58     },
59
60     XML("<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"/>") {
61         @Override
62         String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
63             // Header with namespace declarations for each module
64             final var sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
65                 + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"");
66             for (int i = 0; i < rpcsByPrefix.size(); ++i) {
67                 final var prefix = "ns" + i;
68                 sb.append("\n            xmlns:").append(prefix).append("=\"").append(rpcsByPrefix.get(i).getKey())
69                     .append("\"");
70             }
71             sb.append(" >");
72
73             // Second pass: emit all leaves
74             for (int i = 0; i < rpcsByPrefix.size(); ++i) {
75                 final var prefix = "ns" + i;
76                 for (var localName : rpcsByPrefix.get(i).getValue()) {
77                     sb.append("\n  <").append(prefix).append(':').append(localName).append("/>");
78                 }
79             }
80
81             return sb.append("\n</operations>").toString();
82         }
83
84         @Override
85         String prefix(final ModuleEffectiveStatement module) {
86             return module.localQNameModule().getNamespace().toString();
87         }
88     };
89
90     private final @NonNull String emptyBody;
91
92     OperationsContent(final String emptyBody) {
93         this.emptyBody = requireNonNull(emptyBody);
94     }
95
96     /**
97      * Return the content for a particular {@link EffectiveModelContext}.
98      *
99      * @param context Context to use
100      * @return Content of HTTP GET operation as a String
101      */
102     public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
103         if (context == null) {
104             // Defensive, return empty content
105             return emptyBody;
106         }
107         final var modules = context.getModuleStatements();
108         if (modules.isEmpty()) {
109             // No modules, return empty content
110             return emptyBody;
111         }
112
113         final var moduleRpcs = modules.values().stream()
114             // Extract XMLNamespaces
115             .map(module -> module.localQNameModule().getNamespace())
116             // Make sure each is XMLNamespace unique
117             .distinct()
118             // Find the most recent module with that namespace. This needed so we expose the right set of RPCs,
119             // as we always pick the latest revision to resolve prefix (or module name).
120             .map(namespace -> context.findModuleStatements(namespace).iterator().next())
121             // Convert to module prefix + List<String> with RPC names
122             .map(module -> Map.entry(prefix(module), module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
123                 .map(rpc -> rpc.argument().getLocalName())
124                 .toList()))
125             // Skip prefixes which do not have any RPCs
126             .filter(entry -> !entry.getValue().isEmpty())
127             // Ensure stability: sort by prefix
128             .sorted(Entry.comparingByKey())
129             .toList();
130
131         return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
132     }
133
134     abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
135
136     abstract @NonNull String prefix(ModuleEffectiveStatement module);
137 }