/*
* Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
import static java.util.Objects.requireNonNull;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
/**
* RESTCONF {@code /operations} content for a {@code GET} operation as per
* RFC8040.
*/
enum OperationsContent {
JSON("{ \"ietf-restconf:operations\" : { } }") {
@Override
String createBody(final List>> rpcsByPrefix) {
final var sb = new StringBuilder("{\n"
+ " \"ietf-restconf:operations\" : {\n");
var entryIt = rpcsByPrefix.iterator();
var entry = entryIt.next();
var nameIt = entry.getValue().iterator();
while (true) {
sb.append(" \"").append(entry.getKey()).append(':').append(nameIt.next()).append("\": [null]");
if (nameIt.hasNext()) {
sb.append(",\n");
continue;
}
if (entryIt.hasNext()) {
sb.append(",\n");
entry = entryIt.next();
nameIt = entry.getValue().iterator();
continue;
}
break;
}
return sb.append("\n }\n}").toString();
}
@Override
String prefix(final ModuleEffectiveStatement module) {
return module.argument().getLocalName();
}
},
XML("") {
@Override
String createBody(final List>> rpcsByPrefix) {
// Header with namespace declarations for each module
final var sb = new StringBuilder("\n"
+ "");
// Second pass: emit all leaves
for (int i = 0; i < rpcsByPrefix.size(); ++i) {
final var prefix = "ns" + i;
for (var localName : rpcsByPrefix.get(i).getValue()) {
sb.append("\n <").append(prefix).append(':').append(localName).append("/>");
}
}
return sb.append("\n").toString();
}
@Override
String prefix(final ModuleEffectiveStatement module) {
return module.localQNameModule().getNamespace().toString();
}
};
private final @NonNull String emptyBody;
OperationsContent(final String emptyBody) {
this.emptyBody = requireNonNull(emptyBody);
}
/**
* Return the content for a particular {@link EffectiveModelContext}.
*
* @param context Context to use
* @return Content of HTTP GET operation as a String
*/
public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
if (isEmptyContext(context)) {
// No modules, or defensive return empty content
return emptyBody;
}
final var moduleRpcs = getModuleRpcs(context, context.getModuleStatements());
return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
}
/**
* Return content with RPCs and actions for a particular {@link InstanceIdentifierContext}.
*
* @param identifierContext InstanceIdentifierContext to use
* @return Content of HTTP GET operation as a String
*/
public final @NonNull String bodyFor(final @NonNull InstanceIdentifierContext identifierContext) {
final var context = identifierContext.getSchemaContext();
if (isEmptyContext(context)) {
// No modules, or defensive return empty content
return emptyBody;
}
final var stack = identifierContext.inference().toSchemaInferenceStack();
// empty stack == get all RPCs/actions
if (stack.isEmpty()) {
return createBody(getModuleRpcs(context, context.getModuleStatements()));
}
// get current module RPCs/actions by RPC/action name
final var currentModule = stack.currentModule();
final var currentModuleKey = Map.of(currentModule.localQNameModule(), currentModule);
final var rpcName = identifierContext.getSchemaNode().getQName().getLocalName();
return getModuleRpcs(context, currentModuleKey).stream()
.findFirst()
.map(e -> Map.entry(e.getKey(), e.getValue().stream().filter(rpcName::equals).toList()))
.map(e -> createBody(List.of(e)))
.orElse(emptyBody);
}
private static boolean isEmptyContext(final EffectiveModelContext context) {
if (context == null) {
return true;
}
return context.getModuleStatements().isEmpty();
}
/**
* Returns a list of entries, where each entry contains a module prefix and a list of RPC names.
*
* @param context the effective model context
* @param modules the map of QNameModule to ModuleEffectiveStatement
* @return a list of entries, where each entry contains a module prefix and a list of RPC names
*/
private List>> getModuleRpcs(final EffectiveModelContext context,
final Map modules) {
return modules.values().stream()
// Extract XMLNamespaces
.map(module -> module.localQNameModule().getNamespace())
// Make sure each is XMLNamespace unique
.distinct()
// Find the most recent module with that namespace. This needed so we expose the right set of RPCs,
// as we always pick the latest revision to resolve prefix (or module name).
.map(namespace -> context.findModuleStatements(namespace).iterator().next())
// Convert to module prefix + List with RPC names
.map(module -> Map.entry(prefix(module),
module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
.map(rpc -> rpc.argument().getLocalName())
.toList()))
// Skip prefixes which do not have any RPCs
.filter(entry -> !entry.getValue().isEmpty())
// Ensure stability: sort by prefix
.sorted(Entry.comparingByKey())
.toList();
}
abstract @NonNull String createBody(List>> rpcsByPrefix);
abstract @NonNull String prefix(ModuleEffectiveStatement module);
}