Eliminate RestconfSchemaServiceImpl 52/109052/5
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 23 Nov 2023 10:47:33 +0000 (11:47 +0100)
committerRobert Varga <nite@hq.sk>
Sun, 26 Nov 2023 11:34:22 +0000 (11:34 +0000)
RestconfSchemaServiceImpl is serving YANG/YIN schema sources. While this
capability is not standardized, integrate it into RestconfServer as
modules{Yin,Yang}GET().

This turns out to be a major refactor, as the implementation was shoddy
-- it required local DOMSchemaService to expose
DOMYangTextSourceProvider extension and then it used that when talking
to mount points.

This refactor integrates the ability to lookup sources into
DatabindContext, with the corresponding DOMDatabindProvider wiring. If
no DOMYangTextSourceProvider is present, we call back to using
YangTextSnippet.

JIRA: NETCONF-773
Change-Id: I57689b2915eda0486c0bb5d5ba6de992b3107c0b
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
19 files changed:
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconfCallback.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/RestconfApplication.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/DatabindContext.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/YangCharSource.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/YinCharSource.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractSchemaExportBodyWriter.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/YangSchemaExportBodyWriter.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/YinSchemaExportBodyWriter.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/SchemaExportContext.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfSchemaServiceImpl.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfSchemaSourceUrlProvider.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ModulesGetResult.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/DOMDatabindProvider.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/DOMSourceResolver.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfModulesGetTest.java [moved from restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfModulesGetTest.java with 50% similarity]
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfSchemaServiceTest.java [moved from restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfSchemaServiceTest.java with 71% similarity]

index 9bfcf64db6cd1df3733fd191adeff8be065b16fd..166b578be8203a92d3e88279a698b6c4a60c6762 100644 (file)
@@ -9,7 +9,9 @@ package org.opendaylight.restconf.nb.jaxrs;
 
 import static java.util.Objects.requireNonNull;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.Reader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.text.ParseException;
@@ -42,6 +44,7 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.api.MediaTypes;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
 import org.opendaylight.restconf.common.patch.PatchStatusContext;
@@ -64,11 +67,13 @@ import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
+import org.opendaylight.restconf.server.api.ModulesGetResult;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.RestconfServer;
 import org.opendaylight.restconf.server.spi.OperationOutput;
 import org.opendaylight.yangtools.yang.common.Empty;
 import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.common.YangConstants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -132,7 +137,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
             @Suspended final AsyncResponse ar) {
         server.dataDELETE(identifier).addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            protected Response transform(final Empty result) {
+            Response transform(final Empty result) {
                 return Response.noContent().build();
             }
         });
@@ -184,7 +189,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
             final ReadDataParams readParams, final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            protected Response transform(final NormalizedNodePayload result) {
+            Response transform(final NormalizedNodePayload result) {
                 return switch (readParams.content()) {
                     case ALL, CONFIG -> {
                         final var type = result.data().name().getNodeType();
@@ -289,7 +294,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
     private static void completeDataPATCH(final RestconfFuture<Empty> future, final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            protected Response transform(final Empty result) {
+            Response transform(final Empty result) {
                 return Response.ok().build();
             }
         });
@@ -382,7 +387,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
     private static void completeDataYangPATCH(final RestconfFuture<PatchStatusContext> future, final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            protected Response transform(final PatchStatusContext result) {
+            Response transform(final PatchStatusContext result) {
                 return Response.status(statusOf(result)).entity(result).build();
             }
 
@@ -496,7 +501,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
             final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
             @Override
-            protected Response transform(final DataPostResult result) {
+            Response transform(final DataPostResult result) {
                 if (result instanceof CreateResource createResource) {
                     return Response.created(uriInfo.getBaseUriBuilder()
                             .path("data")
@@ -600,7 +605,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
     private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            protected Response transform(final DataPutResult result) {
+            Response transform(final DataPutResult result) {
                 return switch (result) {
                     // Note: no Location header, as it matches the request path
                     case CREATED -> Response.status(Status.CREATED).build();
@@ -674,7 +679,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
             final Function<OperationsGetResult, String> toString) {
         future.addCallback(new JaxRsRestconfCallback<OperationsGetResult>(ar) {
             @Override
-            protected Response transform(final OperationsGetResult result) {
+            Response transform(final OperationsGetResult result) {
                 return Response.ok().entity(toString.apply(result)).build();
             }
         });
@@ -744,7 +749,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
         server.operationsPOST(uriInfo.getBaseUri(), identifier, body)
             .addCallback(new JaxRsRestconfCallback<OperationOutput>(ar) {
                 @Override
-                protected Response transform(final OperationOutput result) {
+                Response transform(final OperationOutput result) {
                     final var body = result.output();
                     return body == null ? Response.noContent().build()
                         : Response.ok().entity(new NormalizedNodePayload(result.operation(), body)).build();
@@ -769,9 +774,65 @@ public final class JaxRsRestconf implements ParamConverterProvider {
     public void yangLibraryVersionGET(@Suspended final AsyncResponse ar) {
         server.yangLibraryVersionGET().addCallback(new JaxRsRestconfCallback<NormalizedNodePayload>(ar) {
             @Override
-            protected Response transform(final NormalizedNodePayload result) {
+            Response transform(final NormalizedNodePayload result) {
                 return Response.ok().entity(result).build();
             }
         });
     }
+
+    // FIXME: References to these resources are generated by our yang-library implementation. That means:
+    //        - We really need to formalize the parameter structure so we get some help from JAX-RS during matching
+    //          of three things:
+    //          - optional yang-ext:mount prefix(es)
+    //          - mandatory module name
+    //          - optional module revision
+    //        - We really should use /yang-library-module/{name}(/{revision})?
+    //        - We seem to be lacking explicit support for submodules in there -- and those locations should then point
+    //          to /yang-library-submodule/{moduleName}(/{moduleRevision})?/{name}(/{revision})? so as to look the
+    //          submodule up efficiently and allow for the weird case where there are two submodules with the same name
+    //          (that is currently not supported by the parser, but it will be in the future)
+    //        - It does not make sense to support yang-ext:mount, unless we also intercept mount points and rewrite
+    //          yang-library locations. We most likely want to do that to ensure users are not tempted to connect to
+    //          wild destinations
+
+    /**
+     * Get schema of specific module.
+     *
+     * @param identifier path parameter
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @GET
+    @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
+    @Path("/modules/{identifier:.+}")
+    public void modulesYangGET(@PathParam("identifier") final String identifier, @Suspended final AsyncResponse ar) {
+        completeModulesGET(server.modulesYangGET(identifier), ar);
+    }
+
+    /**
+     * Get schema of specific module.
+     *
+     * @param identifier path parameter
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @GET
+    @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
+    @Path("/modules/{identifier:.+}")
+    public void modulesYinGET(@PathParam("identifier") final String identifier, @Suspended final AsyncResponse ar) {
+        completeModulesGET(server.modulesYinGET(identifier), ar);
+    }
+
+    private static void completeModulesGET(final RestconfFuture<ModulesGetResult> future, final AsyncResponse ar) {
+        future.addCallback(new JaxRsRestconfCallback<>(ar) {
+            @Override
+            Response transform(final ModulesGetResult result) {
+                final Reader reader;
+                try {
+                    reader = result.source().openStream();
+                } catch (IOException e) {
+                    throw new RestconfDocumentedException("Cannot open source", e);
+                }
+                return Response.ok(reader).build();
+            }
+        });
+    }
 }
index c24c90b6675ff61f68d7c100bfb67eede7e55401..b60f3481137fede14e8127ee98ba4fb8738f0306 100644 (file)
@@ -19,18 +19,23 @@ import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
  *
  * @param <V> value type
  */
-// FIXME: hide this class
-public abstract class JaxRsRestconfCallback<V> extends RestconfCallback<V> {
+abstract class JaxRsRestconfCallback<V> extends RestconfCallback<V> {
     private final AsyncResponse ar;
 
-    // FIXME: hide this constructor
-    protected JaxRsRestconfCallback(final AsyncResponse ar) {
+    JaxRsRestconfCallback(final AsyncResponse ar) {
         this.ar = requireNonNull(ar);
     }
 
     @Override
     public final void onSuccess(final V result) {
-        ar.resume(transform(result));
+        final Response response;
+        try {
+            response = transform(result);
+        } catch (RestconfDocumentedException e) {
+            onFailure(e);
+            return;
+        }
+        ar.resume(response);
     }
 
     @Override
@@ -38,6 +43,5 @@ public abstract class JaxRsRestconfCallback<V> extends RestconfCallback<V> {
         ar.resume(failure);
     }
 
-    // FIXME: hide this method and its implementations
-    protected abstract Response transform(V result);
+    abstract Response transform(V result) throws RestconfDocumentedException;
 }
index e47a23566dbd55bec9327504a728c7af6b2a346c..89b75006a449f7989417320212c5ad68f1478349 100644 (file)
@@ -17,10 +17,7 @@ import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonNormalizedNodeB
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonPatchStatusBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.XmlNormalizedNodeBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.XmlPatchStatusBodyWriter;
-import org.opendaylight.restconf.nb.rfc8040.jersey.providers.YangSchemaExportBodyWriter;
-import org.opendaylight.restconf.nb.rfc8040.jersey.providers.YinSchemaExportBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.errors.RestconfDocumentedExceptionMapper;
-import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfSchemaServiceImpl;
 import org.opendaylight.restconf.server.api.RestconfServer;
 
 final class RestconfApplication extends Application {
@@ -30,15 +27,13 @@ final class RestconfApplication extends Application {
             final DOMMountPointService mountPointService, final DOMSchemaService domSchemaService) {
         singletons = Set.of(
             new RestconfDocumentedExceptionMapper(databindProvider),
-            new JaxRsRestconf(server),
-            new RestconfSchemaServiceImpl(domSchemaService, mountPointService));
+            new JaxRsRestconf(server));
     }
 
     @Override
     public Set<Class<?>> getClasses() {
         return Set.of(
             JsonNormalizedNodeBodyWriter.class, XmlNormalizedNodeBodyWriter.class,
-            YinSchemaExportBodyWriter.class, YangSchemaExportBodyWriter.class,
             JsonPatchStatusBodyWriter.class, XmlPatchStatusBodyWriter.class);
     }
 
index 9acf3ed3aa57d4afa47396f9f91665115f2a513e..d3d0352559a775dab42b70482d87dc950f76f807 100644 (file)
@@ -9,19 +9,53 @@ package org.opendaylight.restconf.nb.rfc8040.databind;
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.io.CharSource;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
 import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.data.api.schema.MountPointContext;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
 import org.opendaylight.yangtools.yang.data.codec.xml.XmlCodecFactory;
 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.SubmoduleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+import org.opendaylight.yangtools.yang.model.repo.api.YinTextSchemaSource;
 
 /**
  * An immutable context holding a consistent view of things related to data bind operations.
  */
 public final class DatabindContext {
+    /**
+     * Interface for acquiring model source.
+     */
+    @NonNullByDefault
+    @FunctionalInterface
+    public interface SourceResolver {
+        /**
+         * Resolve a specified source into a byte stream in specified representation.
+         *
+         * @param source Source identifier
+         * @param representation Requested representation
+         * @return A {@link RestconfFuture} completing with an {@link CharSource}, or {@code null} if the requested
+         *         representation is not supported.
+         */
+        @Nullable RestconfFuture<CharSource> resolveSource(SourceIdentifier source,
+            Class<? extends SchemaSourceRepresentation> representation);
+    }
+
     private static final VarHandle JSON_CODECS;
     private static final VarHandle XML_CODECS;
 
@@ -36,22 +70,35 @@ public final class DatabindContext {
     }
 
     private final @NonNull MountPointContext mountContext;
+    private final SourceResolver sourceResolver;
 
     @SuppressWarnings("unused")
     private volatile JSONCodecFactory jsonCodecs;
     @SuppressWarnings("unused")
     private volatile XmlCodecFactory xmlCodecs;
 
-    private DatabindContext(final @NonNull MountPointContext mountContext) {
+    private DatabindContext(final @NonNull MountPointContext mountContext,
+            final @Nullable SourceResolver sourceResolver) {
         this.mountContext = requireNonNull(mountContext);
+        this.sourceResolver = sourceResolver;
     }
 
     public static @NonNull DatabindContext ofModel(final EffectiveModelContext modelContext) {
-        return ofMountPoint(MountPointContext.of(modelContext));
+        return ofModel(modelContext, null);
+    }
+
+    public static @NonNull DatabindContext ofModel(final EffectiveModelContext modelContext,
+            final @Nullable SourceResolver sourceResolver) {
+        return ofMountPoint(MountPointContext.of(modelContext), sourceResolver);
     }
 
     public static @NonNull DatabindContext ofMountPoint(final MountPointContext mountContext) {
-        return new DatabindContext(mountContext);
+        return ofMountPoint(mountContext, null);
+    }
+
+    public static @NonNull DatabindContext ofMountPoint(final MountPointContext mountContext,
+            final @Nullable SourceResolver sourceResolver) {
+        return new DatabindContext(mountContext, sourceResolver);
     }
 
     public @NonNull EffectiveModelContext modelContext() {
@@ -79,4 +126,53 @@ public final class DatabindContext {
         final var witness = (XmlCodecFactory) XML_CODECS.compareAndExchangeRelease(this, null, created);
         return witness != null ? witness : created;
     }
+
+    public @NonNull RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
+            final Class<? extends SchemaSourceRepresentation> representation) {
+        final var src = requireNonNull(source);
+        if (sourceResolver != null) {
+            final var delegate = sourceResolver.resolveSource(src, representation);
+            if (delegate != null) {
+                return delegate;
+            }
+        }
+        if (YangTextSchemaSource.class.isAssignableFrom(representation)) {
+            return exportSource(mountContext.getEffectiveModelContext(), source, YangCharSource::new,
+                YangCharSource::new);
+        }
+        if (YinTextSchemaSource.class.isAssignableFrom(representation)) {
+            return exportSource(mountContext.getEffectiveModelContext(), source, YinCharSource.OfModule::new,
+                YinCharSource.OfSubmodule::new);
+        }
+        return RestconfFuture.failed(new RestconfDocumentedException(
+            "Unsupported source representation " + representation.getName()));
+    }
+
+    private static @NonNull RestconfFuture<CharSource> exportSource(final EffectiveModelContext modelContext,
+            final SourceIdentifier source, final Function<ModuleEffectiveStatement, CharSource> moduleCtor,
+            final BiFunction<ModuleEffectiveStatement, SubmoduleEffectiveStatement, CharSource> submoduleCtor) {
+        // If the source identifies a module, things are easy
+        final var name = source.name().getLocalName();
+        final var optRevision = Optional.ofNullable(source.revision());
+        final var optModule = modelContext.findModule(name, optRevision);
+        if (optModule.isPresent()) {
+            return RestconfFuture.of(moduleCtor.apply(optModule.orElseThrow().asEffectiveStatement()));
+        }
+
+        // The source could be a submodule, which we need to hunt down
+        for (var module : modelContext.getModules()) {
+            for (var submodule : module.getSubmodules()) {
+                if (name.equals(submodule.getName()) && optRevision.equals(submodule.getRevision())) {
+                    return RestconfFuture.of(submoduleCtor.apply(module.asEffectiveStatement(),
+                        submodule.asEffectiveStatement()));
+                }
+            }
+        }
+
+        final var sb = new StringBuilder().append("Source ").append(source.name().getLocalName());
+        optRevision.ifPresent(rev -> sb.append('@').append(rev));
+        sb.append(" not found");
+        return RestconfFuture.failed(new RestconfDocumentedException(sb.toString(),
+            ErrorType.APPLICATION, ErrorTag.DATA_MISSING));
+    }
 }
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/YangCharSource.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/YangCharSource.java
new file mode 100644 (file)
index 0000000..65d735b
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 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.databind;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.io.CharSource;
+import java.io.Reader;
+import java.io.StringReader;
+import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.export.DeclaredStatementFormatter;
+import org.opendaylight.yangtools.yang.model.export.YangTextSnippet;
+
+final class YangCharSource extends CharSource {
+    private static final DeclaredStatementFormatter FORMATTER = DeclaredStatementFormatter.builder()
+        .retainDefaultStatements()
+        .build();
+
+    private final YangTextSnippet snippet;
+
+    private YangCharSource(final YangTextSnippet snippet) {
+        this.snippet = requireNonNull(snippet);
+    }
+
+    YangCharSource(final ModuleEffectiveStatement module) {
+        this(FORMATTER.toYangTextSnippet(module, module.getDeclared()));
+    }
+
+    YangCharSource(final ModuleEffectiveStatement module, final SubmoduleEffectiveStatement submodule) {
+        this(FORMATTER.toYangTextSnippet(submodule, submodule.getDeclared()));
+    }
+
+    @Override
+    public Reader openStream() {
+        // FIXME: improve this by implementing a Reader which funnels from Iterator<String>
+        return new StringReader(snippet.toString());
+    }
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/YinCharSource.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/YinCharSource.java
new file mode 100644 (file)
index 0000000..3ef274b
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2023 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.databind;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.io.CharSource;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import javax.xml.stream.XMLStreamException;
+import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.export.YinExportUtils;
+
+abstract sealed class YinCharSource extends CharSource {
+    static final class OfModule extends YinCharSource {
+        private final ModuleEffectiveStatement module;
+
+        OfModule(final ModuleEffectiveStatement module) {
+            this.module = requireNonNull(module);
+        }
+
+        @Override
+        void writeTo(final OutputStream out) throws XMLStreamException {
+            YinExportUtils.writeModuleAsYinText(module, out);
+        }
+    }
+
+    static final class OfSubmodule extends YinCharSource {
+        private final ModuleEffectiveStatement module;
+        private final SubmoduleEffectiveStatement submodule;
+
+        OfSubmodule(final ModuleEffectiveStatement module, final SubmoduleEffectiveStatement submodule) {
+            this.module = requireNonNull(module);
+            this.submodule = requireNonNull(submodule);
+        }
+
+        @Override
+        void writeTo(final OutputStream out) throws XMLStreamException {
+            YinExportUtils.writeSubmoduleAsYinText(module, submodule, out);
+        }
+    }
+
+    @Override
+    public final Reader openStream() throws IOException {
+        final var bos = new ByteArrayOutputStream();
+        try {
+            writeTo(bos);
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to export source", e);
+        }
+        return new StringReader(new String(bos.toByteArray(), StandardCharsets.UTF_8));
+    }
+
+    abstract void writeTo(OutputStream out) throws XMLStreamException;
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractSchemaExportBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractSchemaExportBodyWriter.java
deleted file mode 100644 (file)
index f5aa413..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.jersey.providers;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Type;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.ext.MessageBodyWriter;
-import org.opendaylight.restconf.nb.rfc8040.legacy.SchemaExportContext;
-
-abstract class AbstractSchemaExportBodyWriter implements MessageBodyWriter<SchemaExportContext> {
-    @Override
-    public final boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
-            final MediaType mediaType) {
-        return type.equals(SchemaExportContext.class);
-    }
-}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/YangSchemaExportBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/YangSchemaExportBodyWriter.java
deleted file mode 100644 (file)
index a1edeb6..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (c) 2014 Cisco Systems, Inc. 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.jersey.providers;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Type;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.ExecutionException;
-import javax.ws.rs.Produces;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.ext.Provider;
-import org.opendaylight.restconf.nb.rfc8040.legacy.SchemaExportContext;
-import org.opendaylight.yangtools.yang.common.YangConstants;
-import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
-import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
-
-@Provider
-@Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
-public class YangSchemaExportBodyWriter extends AbstractSchemaExportBodyWriter {
-    @Override
-    public void writeTo(final SchemaExportContext context, final Class<?> type, final Type genericType,
-            final Annotation[] annotations, final MediaType mediaType,
-            final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException {
-        final var module = context.module();
-        final var sourceId = new SourceIdentifier(module.argument(),
-            module.localQNameModule().getRevision().orElse(null));
-        final YangTextSchemaSource yangTextSchemaSource;
-        try {
-            yangTextSchemaSource = context.sourceProvider().getSource(sourceId).get();
-        } catch (InterruptedException | ExecutionException e) {
-            throw new WebApplicationException("Unable to retrieve source from SourceProvider.", e);
-        }
-        yangTextSchemaSource.asByteSource(StandardCharsets.UTF_8).copyTo(entityStream);
-    }
-}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/YinSchemaExportBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/YinSchemaExportBodyWriter.java
deleted file mode 100644 (file)
index bc90a9f..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (c) 2014 Cisco Systems, Inc. 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.jersey.providers;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Type;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.ext.Provider;
-import javax.xml.stream.XMLStreamException;
-import org.opendaylight.restconf.nb.rfc8040.legacy.SchemaExportContext;
-import org.opendaylight.yangtools.yang.common.YangConstants;
-import org.opendaylight.yangtools.yang.model.export.YinExportUtils;
-
-@Provider
-@Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
-public class YinSchemaExportBodyWriter extends AbstractSchemaExportBodyWriter {
-    @Override
-    public void writeTo(final SchemaExportContext context, final Class<?> type, final Type genericType,
-            final Annotation[] annotations, final MediaType mediaType,
-            final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException {
-        try {
-            YinExportUtils.writeModuleAsYinText(context.module(), entityStream);
-        } catch (final XMLStreamException e) {
-            throw new IOException("Failed to export module", e);
-        }
-    }
-}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/SchemaExportContext.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/SchemaExportContext.java
deleted file mode 100644 (file)
index ff22e45..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (c) 2014 Cisco Systems, Inc. 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.legacy;
-
-import static java.util.Objects.requireNonNull;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
-import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceProvider;
-
-/**
- * Holder of schema export context.
- */
-@NonNullByDefault
-public record SchemaExportContext(
-    EffectiveModelContext schemaContext,
-    ModuleEffectiveStatement module,
-    SchemaSourceProvider<YangTextSchemaSource> sourceProvider) {
-
-    public SchemaExportContext {
-        requireNonNull(schemaContext);
-        requireNonNull(module);
-        requireNonNull(sourceProvider);
-    }
-}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfSchemaServiceImpl.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfSchemaServiceImpl.java
deleted file mode 100644 (file)
index 693b321..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco Systems, Inc. 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 com.google.common.base.Preconditions.checkArgument;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
-import com.google.common.collect.Iterables;
-import java.time.format.DateTimeParseException;
-import java.util.Date;
-import java.util.Locale;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.Response;
-import org.eclipse.jdt.annotation.NonNull;
-import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.mdsal.dom.api.DOMSchemaService;
-import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.common.errors.RestconfFuture;
-import org.opendaylight.restconf.nb.jaxrs.JaxRsRestconfCallback;
-import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
-import org.opendaylight.restconf.nb.rfc8040.legacy.SchemaExportContext;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
-import org.opendaylight.yangtools.yang.common.ErrorTag;
-import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.common.Revision;
-import org.opendaylight.yangtools.yang.common.YangConstants;
-import org.opendaylight.yangtools.yang.common.YangNames;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.Module;
-
-/**
- * Retrieval of the YANG modules which server supports.
- */
-@Path("/")
-public class RestconfSchemaServiceImpl {
-    // FIXME: Remove this constant. All logic relying on this constant should instead rely on YangInstanceIdentifier
-    //        equivalent coming out of argument parsing. This may require keeping List<YangInstanceIdentifier> as the
-    //        nested path split on yang-ext:mount. This splitting needs to be based on consulting the
-    //        EffectiveModelContext and allowing it only where yang-ext:mount is actually used in models.
-    private static final String MOUNT = "yang-ext:mount";
-    private static final Splitter SLASH_SPLITTER = Splitter.on('/');
-
-    private final DOMSchemaService schemaService;
-    private final DOMMountPointService mountPointService;
-    private final DOMYangTextSourceProvider sourceProvider;
-
-    /**
-     * Default constructor.
-     *
-     * @param schemaService a {@link DOMSchemaService}
-     * @param mountPointService a {@link DOMMountPointService}
-     */
-    public RestconfSchemaServiceImpl(final DOMSchemaService schemaService,
-            final DOMMountPointService mountPointService) {
-        this.schemaService = requireNonNull(schemaService);
-        this.mountPointService = requireNonNull(mountPointService);
-        sourceProvider = schemaService.getExtensions().getInstance(DOMYangTextSourceProvider.class);
-        checkArgument(sourceProvider != null, "No DOMYangTextSourceProvider available in %s", schemaService);
-    }
-
-    /**
-     * Get schema of specific module.
-     *
-     * @param identifier path parameter
-     * @param ar {@link AsyncResponse} which needs to be completed with an {@link SchemaExportContext}
-     */
-    @GET
-    @Produces({ YangConstants.RFC6020_YIN_MEDIA_TYPE, YangConstants.RFC6020_YANG_MEDIA_TYPE })
-    @Path("modules/{identifier:.+}")
-    public void getSchema(@PathParam("identifier") final String identifier, @Suspended final AsyncResponse ar) {
-        toSchemaExportContextFromIdentifier(schemaService.getGlobalContext(), identifier, mountPointService,
-            sourceProvider).addCallback(new JaxRsRestconfCallback<>(ar) {
-                @Override
-                protected Response transform(final SchemaExportContext result) {
-                    return Response.ok(result).build();
-                }
-            });
-    }
-
-    /**
-     * Parsing {@link Module} module by {@link String} module name and
-     * {@link Date} revision and from the parsed module create
-     * {@link SchemaExportContext}.
-     *
-     * @param schemaContext
-     *             {@link EffectiveModelContext}
-     * @param identifier
-     *             path parameter
-     * @param domMountPointService
-     *             {@link DOMMountPointService}
-     * @return {@link SchemaExportContext}
-     */
-    @VisibleForTesting
-    static @NonNull RestconfFuture<SchemaExportContext> toSchemaExportContextFromIdentifier(
-            final EffectiveModelContext schemaContext, final String identifier,
-            final DOMMountPointService domMountPointService, final DOMYangTextSourceProvider sourceProvider) {
-        final var pathComponents = SLASH_SPLITTER.split(identifier);
-
-        final var it = pathComponents.iterator();
-        final EffectiveModelContext context;
-        final Object debugName;
-        if (Iterables.contains(pathComponents, MOUNT)) {
-            final var sb = new StringBuilder();
-            while (true) {
-                final var current = it.next();
-                sb.append(current);
-                if (MOUNT.equals(current) || !it.hasNext()) {
-                    break;
-                }
-
-                sb.append('/');
-            }
-
-            final InstanceIdentifierContext point;
-            try {
-                point = ParserIdentifier.toInstanceIdentifier(sb.toString(), schemaContext,
-                    requireNonNull(domMountPointService));
-            } catch (RestconfDocumentedException e) {
-                return RestconfFuture.failed(e);
-            }
-
-            final var mountPoint = point.getMountPoint();
-            debugName = mountPoint.getIdentifier();
-            context = mountPoint.getService(DOMSchemaService.class)
-                .map(DOMSchemaService::getGlobalContext)
-                .orElse(null);
-            if (context == null) {
-                return RestconfFuture.failed(new RestconfDocumentedException(
-                    "Mount point '" + debugName + "' does not have a model context"));
-            }
-        } else {
-            context = requireNonNull(schemaContext);
-            debugName = "controller";
-        }
-
-        // module name has to be an identifier
-        if (!it.hasNext()) {
-            return RestconfFuture.failed(new RestconfDocumentedException("Module name must be supplied",
-                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
-        }
-        final var moduleName = it.next();
-        if (moduleName.isEmpty() || !YangNames.IDENTIFIER_START.matches(moduleName.charAt(0))) {
-            return RestconfFuture.failed(new RestconfDocumentedException(
-                "Identifier must start with character from set 'a-zA-Z_", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
-        }
-        if (moduleName.toUpperCase(Locale.ROOT).startsWith("XML")) {
-            return RestconfFuture.failed(new RestconfDocumentedException(
-                "Identifier must NOT start with XML ignore case", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
-        }
-        if (YangNames.NOT_IDENTIFIER_PART.matchesAnyOf(moduleName.substring(1))) {
-            return RestconfFuture.failed(new RestconfDocumentedException(
-                "Supplied name has not expected identifier format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
-        }
-
-        // YANG Revision-compliant string is required
-        if (!it.hasNext()) {
-            return RestconfFuture.failed(new RestconfDocumentedException("Revision date must be supplied.",
-                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
-        }
-        final Revision revision;
-        try {
-            revision = Revision.of(it.next());
-        } catch (final DateTimeParseException e) {
-            return RestconfFuture.failed(new RestconfDocumentedException(
-                "Supplied revision is not in expected date format YYYY-mm-dd", e));
-        }
-
-        final var optModule = context.findModule(moduleName, revision);
-        if (optModule.isEmpty()) {
-            return RestconfFuture.failed(new RestconfDocumentedException(
-                "Module %s %s cannot be found on %s.".formatted(moduleName, revision, debugName),
-                ErrorType.APPLICATION, ErrorTag.DATA_MISSING));
-        }
-
-        return RestconfFuture.of(new SchemaExportContext(context, optModule.orElseThrow().asEffectiveStatement(),
-            // FIXME: this does not seem right -- mounts should have their own thing
-            sourceProvider));
-    }
-}
index 98cc60c549076074bc4cfd80e163360899f9ea17..2693891fde2e3be612c609f26feb9f5db8b15e9c 100644 (file)
@@ -12,18 +12,19 @@ import javax.inject.Singleton;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.netconf.yanglib.writer.YangLibrarySchemaSourceUrlProvider;
+import org.opendaylight.restconf.nb.jaxrs.JaxRsRestconf;
 import org.opendaylight.restconf.nb.rfc8040.URLConstants;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Uri;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.osgi.service.component.annotations.Component;
 
 /**
- * Component composing schema source URL value on per yang resource basis.
+ * Component composing schema source URL value on per YANG resource basis.
  *
  * <p>The URL is expected to be requested by {@link org.opendaylight.netconf.yanglib.writer.YangLibraryWriter
  * YangLibraryWriter} when yang-library data is being constructed, only default module-set name ("ODL_modules")
  * is supported. The composed URL for resource download expected to be served by
- * {@link RestconfSchemaServiceImpl}.
+ * {@link JaxRsRestconf#modulesYangGET(String, javax.ws.rs.container.AsyncResponse)} et al.
  */
 @Singleton
 @Component(immediate = true, service = YangLibrarySchemaSourceUrlProvider.class)
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ModulesGetResult.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ModulesGetResult.java
new file mode 100644 (file)
index 0000000..a20efbe
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 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.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.io.CharSource;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Result of an {@link RestconfServer#modulesYangGET(String)} invocation.
+ *
+ * @param source A {@link CharSource} containing the body
+ */
+@NonNullByDefault
+public record ModulesGetResult(CharSource source) {
+    public ModulesGetResult {
+        requireNonNull(source);
+    }
+}
index a7d44ea97c9f677c37395c37d2addabbd403fabb..b47c48480a64eb57eee5b108c110101aafe6ae25 100644 (file)
@@ -165,4 +165,8 @@ public interface RestconfServer {
     // FIXME: this is a simple encoding-variadic return, similar to how OperationsContent is handled use a common
     //        construct for both cases -- in this case it carries a yang.common.Revision
     RestconfFuture<NormalizedNodePayload> yangLibraryVersionGET();
+
+    RestconfFuture<ModulesGetResult> modulesYangGET(String identifier);
+
+    RestconfFuture<ModulesGetResult> modulesYinGET(String identifier);
 }
index 283cdfe54470b03b7fbb4a6a1433cbdd5deba4bf..bf9fdd7c07133f7bd49b57e9e52cdb6f143941f2 100644 (file)
@@ -13,6 +13,7 @@ import javax.annotation.PreDestroy;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
 import org.opendaylight.yangtools.concepts.Registration;
@@ -29,6 +30,7 @@ import org.osgi.service.component.annotations.Reference;
 @Singleton
 @Component(service = DatabindProvider.class)
 public final class DOMDatabindProvider implements DatabindProvider, EffectiveModelContextListener, AutoCloseable {
+    private final DOMSourceResolver sourceProvider;
     private final Registration reg;
 
     private volatile DatabindContext currentContext;
@@ -36,6 +38,8 @@ public final class DOMDatabindProvider implements DatabindProvider, EffectiveMod
     @Inject
     @Activate
     public DOMDatabindProvider(@Reference final DOMSchemaService schemaService) {
+        final var ext = schemaService.getExtensions().getInstance(DOMYangTextSourceProvider.class);
+        sourceProvider = ext != null ? new DOMSourceResolver(ext) : null;
         currentContext = DatabindContext.ofModel(schemaService.getGlobalContext());
         reg = schemaService.registerSchemaContextListener(this);
     }
@@ -49,7 +53,7 @@ public final class DOMDatabindProvider implements DatabindProvider, EffectiveMod
     public void onModelContextUpdated(final EffectiveModelContext newModelContext) {
         final var local = currentContext;
         if (local != null && local.modelContext() != newModelContext) {
-            currentContext = DatabindContext.ofModel(newModelContext);
+            currentContext = DatabindContext.ofModel(newModelContext, sourceProvider);
         }
     }
 
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/DOMSourceResolver.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/DOMSourceResolver.java
new file mode 100644 (file)
index 0000000..eba2f32
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 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.mdsal;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.CharSource;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext.SourceResolver;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+
+@VisibleForTesting
+public record DOMSourceResolver(@NonNull DOMYangTextSourceProvider domProvider) implements SourceResolver {
+    public DOMSourceResolver {
+        requireNonNull(domProvider);
+    }
+
+    @Override
+    public RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
+            final Class<? extends SchemaSourceRepresentation> representation) {
+        if (!YangTextSchemaSource.class.isAssignableFrom(representation)) {
+            return null;
+        }
+
+        final var ret = new SettableRestconfFuture<CharSource>();
+        Futures.addCallback(domProvider.getSource(source), new FutureCallback<YangTextSchemaSource>() {
+            @Override
+            public void onSuccess(final YangTextSchemaSource result) {
+                ret.set(result);
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                ret.setFailure(cause instanceof RestconfDocumentedException e ? e
+                    : new RestconfDocumentedException(cause.getMessage(), ErrorType.RPC,
+                        ErrorTag.OPERATION_FAILED, cause));
+            }
+        }, MoreExecutors.directExecutor());
+        return ret;
+    }
+}
\ No newline at end of file
index 76839747eed810e99eff38d8f10f75aa777beb39..616ce5cba4110e0d95a24003fad3fee28f4b7e52 100644 (file)
@@ -10,9 +10,11 @@ package org.opendaylight.restconf.server.mdsal;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
@@ -21,9 +23,11 @@ import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
 import java.net.URI;
+import java.time.format.DateTimeParseException;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
@@ -41,6 +45,8 @@ import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
 import org.opendaylight.mdsal.dom.api.DOMRpcService;
+import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
 import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
@@ -62,10 +68,12 @@ import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
 import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
+import org.opendaylight.restconf.server.api.ModulesGetResult;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.RestconfServer;
 import org.opendaylight.restconf.server.spi.OperationInput;
@@ -81,6 +89,7 @@ import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
+import org.opendaylight.yangtools.yang.common.YangNames;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
@@ -90,6 +99,10 @@ import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+import org.opendaylight.yangtools.yang.model.repo.api.YinTextSchemaSource;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -116,6 +129,15 @@ public final class MdsalRestconfServer implements RestconfServer {
         }
     }
 
+    // FIXME: Remove this constant. All logic relying on this constant should instead rely on YangInstanceIdentifier
+    //        equivalent coming out of argument parsing. This may require keeping List<YangInstanceIdentifier> as the
+    //        nested path split on yang-ext:mount. This splitting needs to be based on consulting the
+    //        EffectiveModelContext and allowing it only where yang-ext:mount is actually used in models.
+    @Deprecated(forRemoval = true)
+    private static final String MOUNT = "yang-ext:mount";
+    @Deprecated(forRemoval = true)
+    private static final Splitter SLASH_SPLITTER = Splitter.on('/');
+
     private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
     private final @NonNull DOMMountPointService mountPointService;
     private final @NonNull DatabindProvider databindProvider;
@@ -380,6 +402,100 @@ public final class MdsalRestconfServer implements RestconfServer {
         return req.strategy().putData(req.path(), req.data(), insert);
     }
 
+    @Override
+    public RestconfFuture<ModulesGetResult> modulesYangGET(final String identifier) {
+        return modulesGET(identifier, YangTextSchemaSource.class);
+    }
+
+    @Override
+    public RestconfFuture<ModulesGetResult> modulesYinGET(final String identifier) {
+        return modulesGET(identifier, YinTextSchemaSource.class);
+    }
+
+    private @NonNull RestconfFuture<ModulesGetResult> modulesGET(final String identifier,
+            final Class<? extends SchemaSourceRepresentation> representation) {
+        final var currentContext = databindProvider.currentContext();
+        final var pathComponents = SLASH_SPLITTER.split(identifier);
+        final var it =  pathComponents.iterator();
+        final DatabindContext databind;
+        final Object debugName;
+        if (Iterables.contains(pathComponents, MOUNT)) {
+            final var sb = new StringBuilder();
+            while (true) {
+                final var current = it.next();
+                sb.append(current);
+                if (MOUNT.equals(current) || !it.hasNext()) {
+                    break;
+                }
+
+                sb.append('/');
+            }
+
+            final InstanceIdentifierContext point;
+            try {
+                point = ParserIdentifier.toInstanceIdentifier(sb.toString(), currentContext.modelContext(),
+                    mountPointService);
+            } catch (RestconfDocumentedException e) {
+                return RestconfFuture.failed(e);
+            }
+
+            final var mountPoint = point.getMountPoint();
+            debugName = mountPoint.getIdentifier();
+            final var optSchemaService = mountPoint.getService(DOMSchemaService.class);
+            if (optSchemaService.isEmpty()) {
+                return RestconfFuture.failed(new RestconfDocumentedException(
+                    "Mount point '" + debugName + "' does not expose models"));
+            }
+            final var schemaService = optSchemaService.orElseThrow();
+            final var modelContext = schemaService.getGlobalContext();
+            if (modelContext == null) {
+                return RestconfFuture.failed(new RestconfDocumentedException(
+                    "Mount point '" + debugName + "' does not have a model context"));
+            }
+            final var domProvider = schemaService.getExtensions().getInstance(DOMYangTextSourceProvider.class);
+            databind = DatabindContext.ofModel(modelContext,
+                domProvider == null ? null : new DOMSourceResolver(domProvider));
+        } else {
+            databind = currentContext;
+            debugName = "controller";
+        }
+
+        // module name has to be an identifier
+        if (!it.hasNext()) {
+            return RestconfFuture.failed(new RestconfDocumentedException("Module name must be supplied",
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
+        }
+        final var moduleName = it.next();
+        if (moduleName.isEmpty() || !YangNames.IDENTIFIER_START.matches(moduleName.charAt(0))) {
+            return RestconfFuture.failed(new RestconfDocumentedException(
+                "Identifier must start with character from set 'a-zA-Z_", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
+        }
+        if (moduleName.toUpperCase(Locale.ROOT).startsWith("XML")) {
+            return RestconfFuture.failed(new RestconfDocumentedException(
+                "Identifier must NOT start with XML ignore case", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
+        }
+        if (YangNames.NOT_IDENTIFIER_PART.matchesAnyOf(moduleName.substring(1))) {
+            return RestconfFuture.failed(new RestconfDocumentedException(
+                "Supplied name has not expected identifier format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
+        }
+
+        // YANG Revision-compliant string is required
+        if (!it.hasNext()) {
+            return RestconfFuture.failed(new RestconfDocumentedException("Revision date must be supplied.",
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
+        }
+        final Revision revision;
+        try {
+            revision = Revision.of(it.next());
+        } catch (final DateTimeParseException e) {
+            return RestconfFuture.failed(new RestconfDocumentedException(
+                "Supplied revision is not in expected date format YYYY-mm-dd", e));
+        }
+
+        return databind.resolveSource(new SourceIdentifier(moduleName, revision), representation)
+            .transform(ModulesGetResult::new);
+    }
+
     @Override
     public RestconfFuture<OperationsGetResult> operationsGET() {
         return operationsGET(databindProvider.currentContext().modelContext());
similarity index 50%
rename from restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfModulesGetTest.java
rename to restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfModulesGetTest.java
index 42c5bad559062c2bdeac9800aaca3a2a64f0783a..05f05c28d812d3d244c882ddb7cbe8b11617572b 100644 (file)
@@ -6,36 +6,32 @@
  * 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;
+package org.opendaylight.restconf.nb.jaxrs;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.doReturn;
 
+import com.google.common.io.CharStreams;
+import java.io.IOException;
+import java.io.Reader;
 import java.util.Optional;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.function.Executable;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.opendaylight.mdsal.dom.api.DOMMountPoint;
-import org.opendaylight.mdsal.dom.api.DOMMountPointService;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
 import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
 import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
 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.Revision;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
 
 @ExtendWith(MockitoExtension.class)
-class RestconfModulesGetTest {
+class RestconfModulesGetTest extends AbstractRestconfTest {
     private static final EffectiveModelContext MODEL_CONTEXT =
         YangParserTestUtils.parseYangResourceDirectory("/parser-identifier");
     private static final EffectiveModelContext MODEL_CONTEXT_ON_MOUNT_POINT =
@@ -50,10 +46,11 @@ class RestconfModulesGetTest {
 
     @Mock
     private DOMYangTextSourceProvider sourceProvider;
-    @Mock
-    private DOMMountPoint mountPoint;
-    @Mock
-    private DOMMountPointService mountPointService;
+
+    @Override
+    EffectiveModelContext modelContext() {
+        return MODEL_CONTEXT;
+    }
 
     /**
      * Positive test of getting <code>SchemaExportContext</code>. Expected module name, revision and namespace are
@@ -61,14 +58,17 @@ class RestconfModulesGetTest {
      */
     @Test
     void toSchemaExportContextFromIdentifierTest() {
-        final var exportContext = RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-            TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, null, sourceProvider).getOrThrow();
-        final var module = exportContext.module();
-        assertNotNull(module);
-        assertEquals(TEST_MODULE_NAME, module.argument().getLocalName());
-        final var namespace = module.localQNameModule();
-        assertEquals(Revision.ofNullable(TEST_MODULE_REVISION), namespace.getRevision());
-        assertEquals(TEST_MODULE_NAMESPACE, namespace.getNamespace().toString());
+        assertEquals("""
+            module test-module {
+              namespace test:module;
+              prefix testm;
+              yang-version 1;
+              revision 2016-06-02 {
+                description
+                  "Initial revision.";
+              }
+            }
+            """, assertYang(TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION));
     }
 
     /**
@@ -77,14 +77,8 @@ class RestconfModulesGetTest {
      */
     @Test
     void toSchemaExportContextFromIdentifierNotFoundTest() {
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(
-                MODEL_CONTEXT, "not-existing-module" + "/" + "2016-01-01", null, sourceProvider)
-            .getOrThrow());
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
-        assertEquals("Module not-existing-module 2016-01-01 cannot be found on controller.", error.getErrorMessage());
+        final var error = assertError(ar -> restconf.modulesYinGET("not-existing-module/2016-01-01", ar));
+        assertEquals("Source not-existing-module@2016-01-01 not found", error.getErrorMessage());
         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
         assertEquals(ErrorType.APPLICATION, error.getErrorType());
     }
@@ -96,12 +90,7 @@ class RestconfModulesGetTest {
      */
     @Test
     void toSchemaExportContextFromIdentifierInvalidIdentifierNegativeTest() {
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                TEST_MODULE_REVISION + "/" + TEST_MODULE_NAME, null, sourceProvider).getOrThrow());
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
+        final var error = assertError(ar -> restconf.modulesYangGET(TEST_MODULE_REVISION + "/" + TEST_MODULE_NAME, ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -115,15 +104,18 @@ class RestconfModulesGetTest {
     void toSchemaExportContextFromIdentifierMountPointTest() {
         mockMountPoint();
 
-        final var exportContext = RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-            MOUNT_POINT_IDENT + "/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION,
-            mountPointService, sourceProvider).getOrThrow();
-
-        final var module = exportContext.module();
-        assertEquals(TEST_MODULE_NAME, module.argument().getLocalName());
-        final var namespace = module.localQNameModule();
-        assertEquals(Revision.ofNullable(TEST_MODULE_REVISION), namespace.getRevision());
-        assertEquals(TEST_MODULE_NAMESPACE, namespace.getNamespace().toString());
+        final var content = assertYang(MOUNT_POINT_IDENT + "/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION);
+        assertEquals("""
+            module test-module {
+              namespace test:module;
+              prefix testm;
+              yang-version 1;
+              revision 2016-06-02 {
+                description
+                  "Initial revision.";
+              }
+            }
+            """, content);
     }
 
     /**
@@ -135,16 +127,9 @@ class RestconfModulesGetTest {
         mockMountPoint();
         doReturn(MOUNT_IID).when(mountPoint).getIdentifier();
 
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                MOUNT_POINT_IDENT + "/" + "not-existing-module" + "/" + "2016-01-01",
-                mountPointService, sourceProvider)
-            .getOrThrow());
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
-        assertEquals("Module not-existing-module 2016-01-01 cannot be found on "
-            + "/(mount:point?revision=2016-06-02)mount-container/point-number.", error.getErrorMessage());
+        final var error = assertError(
+            ar -> restconf.modulesYangGET(MOUNT_POINT_IDENT + "/" + "not-existing-module" + "/" + "2016-01-01", ar));
+        assertEquals("Source not-existing-module@2016-01-01 not found", error.getErrorMessage());
         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
         assertEquals(ErrorType.APPLICATION, error.getErrorType());
     }
@@ -158,75 +143,17 @@ class RestconfModulesGetTest {
     void toSchemaExportContextFromIdentifierMountPointInvalidIdentifierNegativeTest() {
         mockMountPoint();
 
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                MOUNT_POINT_IDENT + "/" + TEST_MODULE_REVISION + "/" + TEST_MODULE_NAME, mountPointService,
-                sourceProvider).getOrThrow());
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
+        final var error = assertError(
+            ar -> restconf.modulesYangGET(MOUNT_POINT_IDENT + "/" + TEST_MODULE_REVISION + "/" + TEST_MODULE_NAME, ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
     }
 
-    /**
-     * Negative test of getting <code>SchemaExportContext</code> when supplied identifier is null.
-     * <code>NullPointerException</code> is expected. <code>DOMMountPointService</code> is not used.
-     */
-    @Test
-    void toSchemaExportContextFromIdentifierNullIdentifierNegativeTest() {
-        assertThrows(NullPointerException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT, null, null,
-                sourceProvider));
-    }
-
-    /**
-     * Negative test of of getting <code>SchemaExportContext</code> when supplied <code>SchemaContext</code> is
-     * <code>null</code>. Test is expected to fail with <code>NullPointerException</code>.
-     */
-    @Test
-    void toSchemaExportContextFromIdentifierNullSchemaContextNegativeTest() {
-        assertThrows(NullPointerException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(null,
-                TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, null, sourceProvider));
-    }
-
-    /**
-     * Negative test of of getting <code>SchemaExportContext</code> when supplied <code>SchemaContext</code> is
-     * <code>null</code> and identifier specifies module behind mount point. Test is expected to fail with
-     * <code>NullPointerException</code>.
-     */
-    @Test
-    void toSchemaExportContextFromIdentifierMountPointNullSchemaContextNegativeTest() {
-        assertThrows(NullPointerException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(null,
-                MOUNT_POINT_IDENT + "/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, mountPointService,
-                sourceProvider));
-    }
-
-    /**
-     * Negative test of of getting <code>SchemaExportContext</code> when supplied <code>DOMMountPointService</code>
-     * is <code>null</code> and identifier defines module behind mount point. Test is expected to fail with
-     * <code>NullPointerException</code>.
-     */
-    @Test
-    void toSchemaExportContextFromIdentifierNullMountPointServiceNegativeTest() {
-        assertThrows(NullPointerException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(
-                MODEL_CONTEXT, MOUNT_POINT_IDENT + "/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, null,
-                sourceProvider));
-    }
-
     @Test
     void toSchemaExportContextFromIdentifierNullSchemaContextBehindMountPointNegativeTest() {
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                "/yang-ext:mount/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, mountPointService,
-                sourceProvider).getOrThrow());
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
+        final var error = assertError(
+            ar -> restconf.modulesYangGET("/yang-ext:mount/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, ar));
         // FIXME: this should be something different
         assertEquals("Identifier may not be empty", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
@@ -245,9 +172,7 @@ class RestconfModulesGetTest {
      */
     @Test
     void validateAndGetRevisionNotSuppliedTest() {
-        final var error = assertInvalidValue(
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT, "module", null, null)
-            .getOrThrow());
+        final var error = assertInvalidValue("module");
         assertEquals("Revision date must be supplied.", error.getErrorMessage());
     }
 
@@ -257,12 +182,7 @@ class RestconfModulesGetTest {
      */
     @Test
     void validateAndGetRevisionNotParsableTest() {
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                "module/not-parsable-as-date", null, null).getOrThrow());
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
+        final var error = assertError(ar -> restconf.modulesYangGET("module/not-parsable-as-date", ar));
         assertEquals("Supplied revision is not in expected date format YYYY-mm-dd", error.getErrorMessage());
         assertEquals(ErrorType.APPLICATION, error.getErrorType());
         assertEquals(ErrorTag.OPERATION_FAILED, error.getErrorTag());
@@ -276,9 +196,7 @@ class RestconfModulesGetTest {
     void validateAndGetModulNameNotSuppliedTest() {
         mockMountPoint();
 
-        final var error = assertInvalidValue(
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT, MOUNT_POINT_IDENT,
-                mountPointService, null).getOrThrow());
+        final var error = assertInvalidValue(MOUNT_POINT_IDENT);
         assertEquals("Module name must be supplied", error.getErrorMessage());
     }
 
@@ -289,9 +207,7 @@ class RestconfModulesGetTest {
      */
     @Test
     void validateAndGetModuleNameNotParsableFirstTest() {
-        final var error = assertInvalidValue(
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                "01-not-parsable-as-name-on-first-char", null, null).getOrThrow());
+        final var error = assertInvalidValue("01-not-parsable-as-name-on-first-char");
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
     }
 
@@ -302,9 +218,7 @@ class RestconfModulesGetTest {
      */
     @Test
     public void validateAndGetModuleNameNotParsableNextTest() {
-        final var error = assertInvalidValue(
-            () ->  RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
-                "not-parsable-as-name-after-first-char*", null, null).getOrThrow());
+        final var error = assertInvalidValue("not-parsable-as-name-after-first-char*");
         assertEquals("Supplied name has not expected identifier format", error.getErrorMessage());
     }
 
@@ -314,17 +228,20 @@ class RestconfModulesGetTest {
      */
     @Test
     void validateAndGetModuleNameEmptyTest() {
-        final var error = assertInvalidValue(
-            () -> RestconfSchemaServiceImpl.toSchemaExportContextFromIdentifier(MODEL_CONTEXT, "", null, null)
-            .getOrThrow());
+        final var error = assertInvalidValue("");
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
     }
 
-    private static RestconfError assertInvalidValue(final Executable runnable) {
-        final var ex = assertThrows(RestconfDocumentedException.class, runnable);
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        final var error = errors.get(0);
+    private String assertYang(final String identifier) {
+        try (var reader = assertEntity(Reader.class, 200, ar -> restconf.modulesYangGET(identifier, ar))) {
+            return CharStreams.toString(reader);
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private RestconfError assertInvalidValue(final String identifier) {
+        final var error = assertError(ar -> restconf.modulesYangGET(identifier, ar));
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
         return error;
similarity index 71%
rename from restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfSchemaServiceTest.java
rename to restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfSchemaServiceTest.java
index aadfa6aa714f437d27b4be4d8f18b35cafcd0189..22756b53ab901a245a239bdbcd74881eb62ba33b 100644 (file)
@@ -5,32 +5,41 @@
  * 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;
+package org.opendaylight.restconf.nb.jaxrs;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
 import static org.mockito.Mockito.doReturn;
 import static org.opendaylight.restconf.nb.jaxrs.AbstractRestconfTest.assertEntity;
 import static org.opendaylight.restconf.nb.jaxrs.AbstractRestconfTest.assertError;
 
-import com.google.common.collect.ImmutableClassToInstanceMap;
+import com.google.common.io.CharStreams;
+import com.google.common.util.concurrent.Futures;
+import java.io.Reader;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.dom.api.DOMActionService;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMRpcService;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
 import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
 import org.opendaylight.mdsal.dom.broker.DOMMountPointServiceImpl;
 import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
-import org.opendaylight.restconf.nb.rfc8040.legacy.SchemaExportContext;
+import org.opendaylight.restconf.server.mdsal.DOMSourceResolver;
+import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
 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.Revision;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
 
 /**
@@ -56,14 +65,24 @@ public class RestconfSchemaServiceTest {
     private static final EffectiveModelContext SCHEMA_CONTEXT_WITH_MOUNT_POINTS =
         YangParserTestUtils.parseYangResourceDirectory("/modules/mount-points");
 
-    // service under test
-    private RestconfSchemaServiceImpl schemaService;
-
     // handlers
     @Mock
-    private DOMSchemaService mockSchemaService;
+    private DOMSchemaService schemaService;
+    @Mock
+    private DOMYangTextSourceProvider sourceProvider;
+    @Mock
+    private DOMDataBroker dataBroker;
+    @Mock
+    private DOMActionService actionService;
+    @Mock
+    private DOMRpcService rpcService;
+    @Mock
+    private YangTextSchemaSource yangSource;
     @Mock
-    private DOMYangTextSourceProvider mockSourceProvider;
+    private Reader yangReader;
+
+    // service under test
+    private JaxRsRestconf restconf;
 
     @Before
     public void setup() throws Exception {
@@ -77,31 +96,23 @@ public class RestconfSchemaServiceTest {
                 .createMountPoint(YangInstanceIdentifier.of(QName.create("mount:point:2", "2016-01-01", "cont")))
                 .register();
 
-        doReturn(ImmutableClassToInstanceMap.of(DOMYangTextSourceProvider.class, mockSourceProvider))
-            .when(mockSchemaService).getExtensions();
-        schemaService = new RestconfSchemaServiceImpl(mockSchemaService, mountPointService);
+        restconf = new JaxRsRestconf(new MdsalRestconfServer(
+            () -> DatabindContext.ofModel(schemaService.getGlobalContext(), new DOMSourceResolver(sourceProvider)),
+            dataBroker, rpcService, actionService, mountPointService));
     }
 
     /**
      * Get schema with identifier of existing module and check if correct module was found.
      */
     @Test
-    public void getSchemaTest() {
+    public void getSchemaTest() throws Exception {
         // prepare conditions - return not-mount point schema context
-        doReturn(SCHEMA_CONTEXT).when(mockSchemaService).getGlobalContext();
-
-        // make test
-        final var exportContext = assertEntity(SchemaExportContext.class, 200,
-            ar -> schemaService.getSchema(TEST_MODULE, ar));
-
-        // verify
-        assertNotNull(exportContext);
+        doReturn(SCHEMA_CONTEXT).when(schemaService).getGlobalContext();
+        doReturn(Futures.immediateFuture(yangSource)).when(sourceProvider)
+            .getSource(new SourceIdentifier("module1", Revision.of("2014-01-01")));
+        doReturn(yangReader).when(yangSource).openStream();
 
-        final var module = exportContext.module();
-        assertEquals("module1", module.argument().getLocalName());
-        final var namespace = module.localQNameModule();
-        assertEquals(Revision.ofNullable("2014-01-01"), namespace.getRevision());
-        assertEquals("module:1", namespace.getNamespace().toString());
+        assertSame(yangReader, assertEntity(Reader.class, 200, ar -> restconf.modulesYangGET(TEST_MODULE, ar)));
     }
 
     /**
@@ -111,10 +122,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaForNotExistingModuleTest() {
         // prepare conditions - return not-mount point schema context
-        doReturn(SCHEMA_CONTEXT).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT).when(schemaService).getGlobalContext();
 
-        final var error = assertError(ar -> schemaService.getSchema(NOT_EXISTING_MODULE, ar));
-        assertEquals("Module not-existing 2016-01-01 cannot be found on controller.", error.getErrorMessage());
+        final var error = assertError(ar -> restconf.modulesYinGET(NOT_EXISTING_MODULE, ar));
+        assertEquals("Source not-existing@2016-01-01 not found", error.getErrorMessage());
         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
         assertEquals(ErrorType.APPLICATION, error.getErrorType());
     }
@@ -123,22 +134,20 @@ public class RestconfSchemaServiceTest {
      * Get schema with identifier of existing module behind mount point and check if correct module was found.
      */
     @Test
-    public void getSchemaMountPointTest() {
+    public void getSchemaMountPointTest() throws Exception {
         // prepare conditions - return schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
-
-        // make test
-        final var exportContext = assertEntity(SchemaExportContext.class, 200,
-            ar -> schemaService.getSchema(MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT, ar));
-
-        // verify
-        assertNotNull(exportContext);
-
-        final var module = exportContext.module();
-        assertEquals("module1-behind-mount-point", module.argument().getLocalName());
-        final var namespace = module.localQNameModule();
-        assertEquals(Revision.ofNullable("2014-02-03"), namespace.getRevision());
-        assertEquals("module:1:behind:mount:point", namespace.getNamespace().toString());
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
+
+        final var reader = assertEntity(Reader.class, 200,
+            ar -> restconf.modulesYangGET(MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT, ar));
+        assertEquals("""
+            module module1-behind-mount-point {
+              namespace module:1:behind:mount:point;
+              prefix mod1bemopo;
+              revision 2014-02-03;
+              rpc rpc-behind-module1;
+            }
+            """, CharStreams.toString(reader));
     }
 
     /**
@@ -148,11 +157,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaForNotExistingModuleMountPointTest() {
         // prepare conditions - return schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
-        final var error = assertError(ar -> schemaService.getSchema(MOUNT_POINT + NOT_EXISTING_MODULE, ar));
-        assertEquals("Module not-existing 2016-01-01 cannot be found on /(mount:point:1?revision=2016-01-01)cont.",
-            error.getErrorMessage());
+        final var error = assertError(ar -> restconf.modulesYangGET(MOUNT_POINT + NOT_EXISTING_MODULE, ar));
+        assertEquals("Source not-existing@2016-01-01 not found", error.getErrorMessage());
         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
         assertEquals(ErrorType.APPLICATION, error.getErrorType());
     }
@@ -164,12 +172,12 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaNullSchemaContextBehindMountPointTest() {
         // prepare conditions - return correct schema context for mount points (this is not null)
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
         // make test - call service on mount point with null schema context
         // NULL_MOUNT_POINT contains null schema context
         final var error = assertError(
-            ar -> schemaService.getSchema(NULL_MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT, ar));
+            ar -> restconf.modulesYangGET(NULL_MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT, ar));
         assertEquals("Mount point mount-point-2:cont does not expose DOMSchemaService", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTags.RESOURCE_DENIED_TRANSPORT, error.getErrorTag());
@@ -182,9 +190,9 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWithEmptyIdentifierTest() {
         // prepare conditions - return correct schema context
-        doReturn(SCHEMA_CONTEXT).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT).when(schemaService).getGlobalContext();
 
-        final var error = assertError(ar -> schemaService.getSchema("", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET("", ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -198,10 +206,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWithEmptyIdentifierMountPointTest() {
         // prepare conditions - return correct schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema(MOUNT_POINT + "", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET(MOUNT_POINT + "", ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -214,10 +222,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWithNotParsableIdentifierTest() {
         // prepare conditions - return correct schema context without mount points
-        doReturn(SCHEMA_CONTEXT).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema("01_module/2016-01-01", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET("01_module/2016-01-01", ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -231,10 +239,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWithNotParsableIdentifierMountPointTest() {
         // prepare conditions - return correct schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema(MOUNT_POINT + "01_module/2016-01-01", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET(MOUNT_POINT + "01_module/2016-01-01", ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -250,10 +258,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWrongIdentifierTest() {
         // prepare conditions - return correct schema context without mount points
-        doReturn(SCHEMA_CONTEXT).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema("2014-01-01", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET("2014-01-01", ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -270,10 +278,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWrongIdentifierMountPointTest() {
         // prepare conditions - return correct schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema(MOUNT_POINT + "2014-01-01", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET(MOUNT_POINT + "2014-01-01", ar));
         assertEquals("Identifier must start with character from set 'a-zA-Z_", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -287,10 +295,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWithoutRevisionTest() {
         // prepare conditions - return correct schema context without mount points
-        doReturn(SCHEMA_CONTEXT).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema("module", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET("module", ar));
         assertEquals("Revision date must be supplied.", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -304,10 +312,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaWithoutRevisionMountPointTest() {
         // prepare conditions - return correct schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
         // make test and verify
-        final var error = assertError(ar -> schemaService.getSchema(MOUNT_POINT + "module", ar));
+        final var error = assertError(ar -> restconf.modulesYangGET(MOUNT_POINT + "module", ar));
         assertEquals("Revision date must be supplied.", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
@@ -320,10 +328,10 @@ public class RestconfSchemaServiceTest {
     @Test
     public void getSchemaContextWithNotExistingMountPointTest() {
         // prepare conditions - return schema context with mount points
-        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(mockSchemaService).getGlobalContext();
+        doReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS).when(schemaService).getGlobalContext();
 
         final var error = assertError(
-            ar -> schemaService.getSchema(NOT_EXISTING_MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT, ar));
+            ar -> restconf.modulesYangGET(NOT_EXISTING_MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT, ar));
         assertEquals("Failed to lookup for module with name 'mount-point-3'.", error.getErrorMessage());
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
         assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.getErrorTag());