2 * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.collect.HashBasedTable;
13 import java.util.ArrayList;
14 import java.util.Comparator;
15 import java.util.List;
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;
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>.
31 enum OperationsContent {
32 JSON("{ \"ietf-restconf:operations\" : { } }") {
34 String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
35 final var sb = new StringBuilder("{\n"
36 + " \"ietf-restconf:operations\" : {\n");
37 var entryIt = rpcsByPrefix.iterator();
38 var entry = entryIt.next();
39 var nameIt = entry.getValue().iterator();
41 sb.append(" \"").append(entry.getKey()).append(':').append(nameIt.next()).append("\": [null]");
42 if (nameIt.hasNext()) {
47 if (entryIt.hasNext()) {
49 entry = entryIt.next();
50 nameIt = entry.getValue().iterator();
57 return sb.append("\n }\n}").toString();
61 String prefix(final ModuleEffectiveStatement module) {
62 return module.argument().getLocalName();
66 XML("<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"/>") {
68 String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
69 // Header with namespace declarations for each module
70 final var sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
71 + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"");
72 for (int i = 0; i < rpcsByPrefix.size(); ++i) {
73 final var prefix = "ns" + i;
74 sb.append("\n xmlns:").append(prefix).append("=\"").append(rpcsByPrefix.get(i).getKey())
79 // Second pass: emit all leaves
80 for (int i = 0; i < rpcsByPrefix.size(); ++i) {
81 final var prefix = "ns" + i;
82 for (var localName : rpcsByPrefix.get(i).getValue()) {
83 sb.append("\n <").append(prefix).append(':').append(localName).append("/>");
87 return sb.append("\n</operations>").toString();
91 String prefix(final ModuleEffectiveStatement module) {
92 return module.localQNameModule().getNamespace().toString();
96 private final @NonNull String emptyBody;
98 OperationsContent(final String emptyBody) {
99 this.emptyBody = requireNonNull(emptyBody);
103 * Return the content for a particular {@link EffectiveModelContext}.
105 * @param context Context to use
106 * @return Content of HTTP GET operation as a String
108 public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
109 if (context == null) {
112 final var modules = context.getModuleStatements();
113 if (modules.isEmpty()) {
117 // Index into prefix -> revision -> module table
118 final var prefixRevModule = HashBasedTable.<String, Optional<Revision>, ModuleEffectiveStatement>create();
119 for (var module : modules.values()) {
120 prefixRevModule.put(prefix(module), module.localQNameModule().getRevision(), module);
123 // Now extract RPC names for each module with highest revision. This needed so we expose the right set of RPCs,
124 // as we always pick the latest revision to resolve prefix (or module name)
125 // TODO: Simplify this once we have yangtools-7.0.9+
126 final var moduleRpcs = new ArrayList<Entry<String, List<String>>>();
127 for (var moduleEntry : prefixRevModule.rowMap().entrySet()) {
128 final var revisions = new ArrayList<>(moduleEntry.getValue().keySet());
129 revisions.sort(Revision::compare);
130 final var selectedRevision = revisions.get(revisions.size() - 1);
132 final var rpcNames = moduleEntry.getValue().get(selectedRevision)
133 .streamEffectiveSubstatements(RpcEffectiveStatement.class)
134 .map(rpc -> rpc.argument().getLocalName())
135 .collect(Collectors.toUnmodifiableList());
136 if (!rpcNames.isEmpty()) {
137 moduleRpcs.add(Map.entry(moduleEntry.getKey(), rpcNames));
141 if (moduleRpcs.isEmpty()) {
142 // No RPCs, return empty content
146 // Ensure stability: sort by prefix
147 moduleRpcs.sort(Comparator.comparing(Entry::getKey));
149 return modules.isEmpty() ? emptyBody : createBody(moduleRpcs);
152 abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
154 abstract @NonNull String prefix(ModuleEffectiveStatement module);