All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opendaylight.restconf.server.spi.ApiPathCanonizer Maven / Gradle / Ivy

There is a newer version: 8.0.3
Show newest version
/*
 * Copyright (c) 2024 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.server.spi;

import static java.util.Objects.requireNonNull;

import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.api.ApiPath.ApiIdentifier;
import org.opendaylight.restconf.api.ApiPath.ListInstance;
import org.opendaylight.restconf.api.ApiPath.Step;
import org.opendaylight.restconf.server.api.DatabindContext;
import org.opendaylight.restconf.server.api.ServerException;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodec;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.Composite;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;

/**
 * Utility class for turning {@link YangInstanceIdentifier}s into {@link ApiPath}s via
 * {@link #dataToApiPath(YangInstanceIdentifier)}.
 */
public final class ApiPathCanonizer {
    private final @NonNull DatabindContext databind;

    public ApiPathCanonizer(final DatabindContext databind) {
        this.databind = requireNonNull(databind);
    }

    /**
     * Return the canonical {@link ApiPath} for specified {@link YangInstanceIdentifier}.
     *
     * @param path {@link YangInstanceIdentifier} to canonicalize
     * @return An {@link ApiPath}
     * @throws ServerException if an error occurs
     */
    public @NonNull ApiPath dataToApiPath(final YangInstanceIdentifier path) throws ServerException {
        final var it = path.getPathArguments().iterator();
        if (!it.hasNext()) {
            return ApiPath.empty();
        }

        final var stack = SchemaInferenceStack.of(databind.modelContext());
        final var builder = ImmutableList.builder();
        DataSchemaContext context = databind.schemaTree().getRoot();
        QNameModule parentModule = null;
        do {
            final var arg = it.next();

            // get module of the parent
            if (!(context instanceof PathMixin)) {
                parentModule = context.dataSchemaNode().getQName().getModule();
            }

            final var childContext = context instanceof Composite composite ? composite.enterChild(stack, arg) : null;
            if (childContext == null) {
                throw new ServerException(ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT,
                    "Invalid input '%s': schema for argument '%s' (after '%s') not found", path, arg,
                    ApiPath.of(builder.build()));
            }

            context = childContext;

            // only output PathArguments which are not inherent to YangInstanceIdentifier structure
            if (!(childContext instanceof PathMixin)) {
                builder.add(argToStep(arg, parentModule, stack, context));
            }
        } while (it.hasNext());

        return new ApiPath(builder.build());
    }

    private @NonNull Step argToStep(final PathArgument arg, final QNameModule prevNamespace,
            final SchemaInferenceStack stack, final DataSchemaContext context) throws ServerException {
        // append namespace before every node which is defined in other module than its parent
        // condition is satisfied also for the first path argument
        final var nodeType = arg.getNodeType();
        final var module = nodeType.getModule().equals(prevNamespace) ? null : resolvePrefix(nodeType);
        final var identifier = nodeType.unbind();

        // NodeIdentifier maps to an ApiIdentifier
        if (arg instanceof NodeIdentifier) {
            return new ApiIdentifier(module, identifier);
        }

        // NodeWithValue addresses a LeafSetEntryNode and maps to a ListInstance with a single value
        final var schema = context.dataSchemaNode();
        if (arg instanceof NodeWithValue withValue) {
            if (!(schema instanceof LeafListSchemaNode leafList)) {
                throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
                    "Argument '%s' does not map to a leaf-list, but %s", arg, schema);
            }
            return ListInstance.of(module, identifier, valueToString(stack, leafList, withValue.getValue()));
        }

        // The only remaining case is NodeIdentifierWrithPredicates, verify that instead of an explicit cast
        if (!(arg instanceof NodeIdentifierWithPredicates withPredicates)) {
            throw new VerifyException("Unhandled " + arg);
        }
        // A NodeIdentifierWithPredicates adresses a MapEntryNode and maps to a ListInstance with one or more values:
        // 1) schema has to be a ListSchemaNode
        if (!(schema instanceof ListSchemaNode list)) {
            throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
                "Argument '%s' does not map to a list, but %s", arg, schema);
        }
        // 2) the key definition must be non-empty
        final var keyDef = list.getKeyDefinition();
        final var size = keyDef.size();
        if (size == 0) {
            throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
                "Argument '%s' maps a list without any keys %s", arg, schema);
        }
        // 3) the number of predicates has to match the number of keys
        if (size != withPredicates.size()) {
            throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
                "Argument '%s' does not match required keys %s", arg, keyDef);
        }

        // ListSchemaNode implies the context is a composite, verify that instead of an unexplained cast when we look
        // up the schema for individual keys
        if (!(context instanceof Composite composite)) {
            throw new VerifyException("Unexpected non-composite " + context + " with " + list);
        }

        final var builder = ImmutableList.builderWithExpectedSize(size);
        for (var key : keyDef) {
            final var value = withPredicates.getValue(key);
            if (value == null) {
                throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
                    "Argument '%s' is missing predicate for %s", arg, key);
            }

            final var tmpStack = stack.copy();
            final var keyContext = composite.enterChild(tmpStack, key);
            if (keyContext == null) {
                throw new VerifyException("Failed to find key " + key + " in " + composite);
            }
            if (!(keyContext.dataSchemaNode() instanceof LeafSchemaNode leaf)) {
                throw new VerifyException("Key " + key + " maps to non-leaf context " + keyContext);
            }
            builder.add(valueToString(tmpStack, leaf, value));
        }
        return ListInstance.of(module, identifier, builder.build());
    }

    private  @NonNull String valueToString(final SchemaInferenceStack stack, final TypedDataSchemaNode schema,
            final T value) {
        @SuppressWarnings("unchecked")
        final var codec = (JSONCodec) databind.jsonCodecs().codecFor(schema, stack);
        return codec.unparseValue(value).rawString();
    }

    /**
     * Create prefix of namespace from {@link QName}.
     *
     * @param qname {@link QName}
     * @return {@link String}
     */
    private @NonNull String resolvePrefix(final QName qname) {
        return databind.modelContext().findModuleStatement(qname.getModule()).orElseThrow().argument().getLocalName();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy