199728f22346385353a6af10caa158e93950cd7b
[netconf.git] / restconf / restconf-common / src / main / java / org / opendaylight / restconf / common / 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.common;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.HashBasedTable;
13 import java.util.ArrayList;
14 import java.util.Comparator;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Map.Entry;
18 import java.util.Optional;
19 import java.util.stream.Collectors;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.yangtools.yang.common.Revision;
23 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
24 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
25 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
26
27 /**
28  * RESTCONF {@code /operations} content for a {@code GET} operation as per
29  * <a href="https://datatracker.ietf.org/doc/html/rfc8040#section-3.3.2">RFC8040</a>.
30  */
31 // FIXME: when bierman02 is gone, this should be folded to nb-rfc8040, as it is a server-side thing.
32 public enum OperationsContent {
33     JSON("{ \"ietf-restconf:operations\" : { } }") {
34         @Override
35         String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
36             final var sb = new StringBuilder("{\n"
37                 + "  \"ietf-restconf:operations\" : {\n");
38             var entryIt = rpcsByPrefix.iterator();
39             var entry = entryIt.next();
40             var nameIt = entry.getValue().iterator();
41             while (true) {
42                 sb.append("    \"").append(entry.getKey()).append(':').append(nameIt.next()).append("\": [null]");
43                 if (nameIt.hasNext()) {
44                     sb.append(",\n");
45                     continue;
46                 }
47
48                 if (entryIt.hasNext()) {
49                     sb.append(",\n");
50                     entry = entryIt.next();
51                     nameIt = entry.getValue().iterator();
52                     continue;
53                 }
54
55                 break;
56             }
57
58             return sb.append("\n  }\n}").toString();
59         }
60
61         @Override
62         String prefix(final ModuleEffectiveStatement module) {
63             return module.argument().getLocalName();
64         }
65     },
66
67     XML("<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"/>") {
68         @Override
69         String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
70             // Header with namespace declarations for each module
71             final var sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
72                 + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"");
73             for (int i = 0; i < rpcsByPrefix.size(); ++i) {
74                 final var prefix = "ns" + i;
75                 sb.append("\n            xmlns:").append(prefix).append("=\"").append(rpcsByPrefix.get(i).getKey())
76                     .append("\"");
77             }
78             sb.append(" >");
79
80             // Second pass: emit all leaves
81             for (int i = 0; i < rpcsByPrefix.size(); ++i) {
82                 final var prefix = "ns" + i;
83                 for (var localName : rpcsByPrefix.get(i).getValue()) {
84                     sb.append("\n  <").append(prefix).append(':').append(localName).append("/>");
85                 }
86             }
87
88             return sb.append("\n</operations>").toString();
89         }
90
91         @Override
92         String prefix(final ModuleEffectiveStatement module) {
93             return module.localQNameModule().getNamespace().toString();
94         }
95     };
96
97     private final @NonNull String emptyBody;
98
99     OperationsContent(final String emptyBody) {
100         this.emptyBody = requireNonNull(emptyBody);
101     }
102
103     /**
104      * Return the content for a particular {@link EffectiveModelContext}.
105      *
106      * @param context Context to use
107      * @return Content of HTTP GET operation as a String
108      */
109     public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
110         if (context == null) {
111             return emptyBody;
112         }
113         final var modules = context.getModuleStatements();
114         if (modules.isEmpty()) {
115             return emptyBody;
116         }
117
118         // Index into prefix -> revision -> module table
119         final var prefixRevModule = HashBasedTable.<String, Optional<Revision>, ModuleEffectiveStatement>create();
120         for (var module : modules.values()) {
121             prefixRevModule.put(prefix(module), module.localQNameModule().getRevision(), module);
122         }
123
124         // Now extract RPC names for each module with highest revision. This needed so we expose the right set of RPCs,
125         // as we always pick the latest revision to resolve prefix (or module name)
126         // TODO: Simplify this once we have yangtools-7.0.9+
127         final var moduleRpcs = new ArrayList<Entry<String, List<String>>>();
128         for (var moduleEntry : prefixRevModule.rowMap().entrySet()) {
129             final var revisions = new ArrayList<>(moduleEntry.getValue().keySet());
130             revisions.sort(Revision::compare);
131             final var selectedRevision = revisions.get(revisions.size() - 1);
132
133             final var rpcNames = moduleEntry.getValue().get(selectedRevision)
134                 .streamEffectiveSubstatements(RpcEffectiveStatement.class)
135                 .map(rpc -> rpc.argument().getLocalName())
136                 .collect(Collectors.toUnmodifiableList());
137             if (!rpcNames.isEmpty()) {
138                 moduleRpcs.add(Map.entry(moduleEntry.getKey(), rpcNames));
139             }
140         }
141
142         if (moduleRpcs.isEmpty()) {
143             // No RPCs, return empty content
144             return emptyBody;
145         }
146
147         // Ensure stability: sort by prefix
148         moduleRpcs.sort(Comparator.comparing(Entry::getKey));
149
150         return modules.isEmpty() ? emptyBody : createBody(moduleRpcs);
151     }
152
153     abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
154
155     abstract @NonNull String prefix(ModuleEffectiveStatement module);
156 }