From cebda3ec669df9ee4177a8865803151cc7537aae Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Mon, 22 Jan 2024 22:13:39 +0100 Subject: [PATCH] Make RESTCONF base path configurable Move BASE_PATH constant into OSGi configuration to make it configurable and propagate it to other components through RestconfStreamServletFactory. JIRA: NETCONF-1218 Change-Id: Ie1aed49ed37ff3e0cc862db77c7b88c7470c082a Signed-off-by: Oleksandr Zharov Signed-off-by: Robert Varga --- .../restconf/nb/rfc8040/JaxRsNorthbound.java | 4 +-- .../restconf/nb/rfc8040/OSGiNorthbound.java | 9 ++++-- .../restconf/nb/rfc8040/URLConstants.java | 4 --- .../DefaultRestconfStreamServletFactory.java | 25 ++++++++++++---- .../streams/RestconfStreamServletFactory.java | 6 ++++ .../nb/rfc8040/streams/WebSocketFactory.java | 30 +++++++++++-------- .../rfc8040/streams/WebSocketInitializer.java | 6 ++-- .../RestconfSchemaSourceUrlProvider.java | 11 ++++--- .../rfc8040/streams/WebSocketFactoryTest.java | 8 ++--- .../RestconfSchemaSourceUrlProviderTest.java | 24 ++++++++++++--- 10 files changed, 86 insertions(+), 41 deletions(-) diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java index ef6f63d853..2a0a81f452 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java @@ -52,7 +52,7 @@ public final class JaxRsNorthbound implements AutoCloseable { @Reference final RestconfStreamServletFactory servletFactory) throws ServletException { final var restconfBuilder = WebContext.builder() .name("RFC8040 RESTCONF") - .contextPath("/" + URLConstants.BASE_PATH) + .contextPath("/" + servletFactory.restconf()) .supportsSessions(false) .addServlet(ServletDetails.builder() .addUrlPattern("/*") @@ -102,7 +102,7 @@ public final class JaxRsNorthbound implements AutoCloseable { new Application() { @Override public Set getSingletons() { - return Set.of(new JaxRsWebHostMetadata(URLConstants.BASE_PATH)); + return Set.of(new JaxRsWebHostMetadata(servletFactory.restconf())); } }).build()) .name("Rootfound") diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/OSGiNorthbound.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/OSGiNorthbound.java index ffd60a18f7..d83a126bc3 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/OSGiNorthbound.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/OSGiNorthbound.java @@ -51,6 +51,9 @@ public final class OSGiNorthbound { @Deprecated(since = "7.0.0", forRemoval = true) @AttributeDefinition boolean use$_$sse() default true; + @AttributeDefinition(name = "{+restconf}", description = """ + The value of RFC8040 {+restconf} URI template, poiting to the root resource. Must not end with '/'.""") + String restconf() default "rests"; } private static final Logger LOG = LoggerFactory.getLogger(OSGiNorthbound.class); @@ -77,7 +80,8 @@ public final class OSGiNorthbound { useSSE = configuration.use$_$sse(); registry = registryFactory.newInstance(FrameworkUtil.asDictionary(MdsalRestconfStreamRegistry.props(useSSE))); - servletProps = DefaultRestconfStreamServletFactory.props(registry.getInstance(), useSSE, + servletProps = DefaultRestconfStreamServletFactory.props(configuration.restconf(), registry.getInstance(), + useSSE, new StreamsConfiguration(configuration.maximum$_$fragment$_$length(), configuration.idle$_$timeout(), configuration.heartbeat$_$interval()), configuration.ping$_$executor$_$name$_$prefix(), configuration.max$_$thread$_$count()); @@ -97,7 +101,8 @@ public final class OSGiNorthbound { LOG.debug("ListenersBroker restarted with {}", newUseSSE ? "SSE" : "Websockets"); } - final var newServletProps = DefaultRestconfStreamServletFactory.props(registry.getInstance(), useSSE, + final var newServletProps = DefaultRestconfStreamServletFactory.props(configuration.restconf(), + registry.getInstance(), useSSE, new StreamsConfiguration(configuration.maximum$_$fragment$_$length(), configuration.idle$_$timeout(), configuration.heartbeat$_$interval()), configuration.ping$_$executor$_$name$_$prefix(), configuration.max$_$thread$_$count()); diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/URLConstants.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/URLConstants.java index cfe46d8320..4c0f9886bd 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/URLConstants.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/URLConstants.java @@ -14,10 +14,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public final class URLConstants { - /** - * The first URL path element for RESTCONF implementation, i.e. {@code https://localhost/BASE_PATH}. - */ - public static final String BASE_PATH = "rests"; /** * The second URL path element for streams support, i.e. {@code https://localhost/BASE_PATH/STREAMS}. */ diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/DefaultRestconfStreamServletFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/DefaultRestconfStreamServletFactory.java index 55f5875900..a909a76cc4 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/DefaultRestconfStreamServletFactory.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/DefaultRestconfStreamServletFactory.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServlet; import javax.ws.rs.core.Application; +import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.aaa.web.servlet.ServletSupport; import org.opendaylight.restconf.server.spi.RestconfStream; import org.osgi.service.component.annotations.Activate; @@ -41,7 +42,9 @@ public final class DefaultRestconfStreamServletFactory implements RestconfStream private static final String PROP_CORE_POOL_SIZE = ".corePoolSize"; private static final String PROP_USE_WEBSOCKETS = ".useWebsockets"; private static final String PROP_STREAMS_CONFIGURATION = ".streamsConfiguration"; + private static final String PROP_RESTCONF = ".restconf"; + private final @NonNull String restconf; private final RestconfStream.Registry streamRegistry; private final ServletSupport servletSupport; @@ -49,10 +52,14 @@ public final class DefaultRestconfStreamServletFactory implements RestconfStream private final StreamsConfiguration streamsConfiguration; private final boolean useWebsockets; - public DefaultRestconfStreamServletFactory(final ServletSupport servletSupport, + public DefaultRestconfStreamServletFactory(final ServletSupport servletSupport, final String restconf, final RestconfStream.Registry streamRegistry, final StreamsConfiguration streamsConfiguration, final String namePrefix, final int corePoolSize, final boolean useWebsockets) { this.servletSupport = requireNonNull(servletSupport); + this.restconf = requireNonNull(restconf); + if (restconf.endsWith("/")) { + throw new IllegalArgumentException("{+restconf} value ends with /"); + } this.streamRegistry = requireNonNull(streamRegistry); this.streamsConfiguration = requireNonNull(streamsConfiguration); pingExecutor = new DefaultPingExecutor(namePrefix, corePoolSize); @@ -67,15 +74,21 @@ public final class DefaultRestconfStreamServletFactory implements RestconfStream @Activate public DefaultRestconfStreamServletFactory(@Reference final ServletSupport servletSupport, final Map props) { - this(servletSupport, (RestconfStream.Registry) props.get(PROP_STREAM_REGISTRY), + this(servletSupport, (String) props.get(PROP_RESTCONF), + (RestconfStream.Registry) props.get(PROP_STREAM_REGISTRY), (StreamsConfiguration) props.get(PROP_STREAMS_CONFIGURATION), (String) props.get(PROP_NAME_PREFIX), (int) requireNonNull(props.get(PROP_CORE_POOL_SIZE)), (boolean) requireNonNull(props.get(PROP_USE_WEBSOCKETS))); } + @Override + public String restconf() { + return restconf; + } + @Override public HttpServlet newStreamServlet() { - return useWebsockets ? new WebSocketInitializer(streamRegistry, pingExecutor, streamsConfiguration) + return useWebsockets ? new WebSocketInitializer(restconf, streamRegistry, pingExecutor, streamsConfiguration) : servletSupport.createHttpServletBuilder( new Application() { @Override @@ -91,9 +104,11 @@ public final class DefaultRestconfStreamServletFactory implements RestconfStream pingExecutor.close(); } - public static Map props(final RestconfStream.Registry streamRegistry, final boolean useSSE, - final StreamsConfiguration streamsConfiguration, final String namePrefix, final int corePoolSize) { + public static Map props(final String restconf, final RestconfStream.Registry streamRegistry, + final boolean useSSE, final StreamsConfiguration streamsConfiguration, final String namePrefix, + final int corePoolSize) { return Map.of( + PROP_RESTCONF, restconf, PROP_STREAM_REGISTRY, streamRegistry, PROP_USE_WEBSOCKETS, !useSSE, PROP_STREAMS_CONFIGURATION, streamsConfiguration, diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/RestconfStreamServletFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/RestconfStreamServletFactory.java index 1aec86f1d5..14848ad48c 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/RestconfStreamServletFactory.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/RestconfStreamServletFactory.java @@ -19,6 +19,12 @@ import org.opendaylight.restconf.server.spi.RestconfStream; */ @Deprecated(since = "7.0.0", forRemoval = true) public interface RestconfStreamServletFactory { + /** + * Return the value of {@code {+restconf}} macro. May be empty, guaranteed to not end with {@code /}. + * + * @return the value of {@code {+restconf}} macro + */ + @NonNull String restconf(); @NonNull HttpServlet newStreamServlet(); } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactory.java index bd130f6325..f4f0d4a9d1 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactory.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactory.java @@ -28,18 +28,22 @@ import org.slf4j.LoggerFactory; * @param heartbeatInterval Interval in milliseconds between sending of ping control frames. */ @Deprecated(since = "7.0.0", forRemoval = true) -record WebSocketFactory( - RestconfStream.Registry streamRegistry, - PingExecutor pingExecutor, - int maximumFragmentLength, - int heartbeatInterval) implements WebSocketCreator { +final class WebSocketFactory implements WebSocketCreator { private static final Logger LOG = LoggerFactory.getLogger(WebSocketFactory.class); - private static final String STREAMS_PREFIX = - "/" + URLConstants.BASE_PATH + "/" + URLConstants.STREAMS_SUBPATH + "/"; - WebSocketFactory { - requireNonNull(pingExecutor); - requireNonNull(streamRegistry); + private final RestconfStream.Registry streamRegistry; + private final PingExecutor pingExecutor; + private final int maximumFragmentLength; + private final int heartbeatInterval; + private final String streamsPath; + + WebSocketFactory(final String restconf, final RestconfStream.Registry streamRegistry, + final PingExecutor pingExecutor, final int maximumFragmentLength, final int heartbeatInterval) { + this.streamRegistry = requireNonNull(streamRegistry); + this.pingExecutor = requireNonNull(pingExecutor); + this.maximumFragmentLength = maximumFragmentLength; + this.heartbeatInterval = heartbeatInterval; + streamsPath = '/' + requireNonNull(restconf) + '/' + URLConstants.STREAMS_SUBPATH + '/'; } /** @@ -55,12 +59,12 @@ record WebSocketFactory( @Override public Object createWebSocket(final ServletUpgradeRequest req, final ServletUpgradeResponse resp) { final var path = req.getRequestURI().getPath(); - if (!path.startsWith(STREAMS_PREFIX)) { - LOG.debug("Request path '{}' does not start with '{}'", path, STREAMS_PREFIX); + if (!path.startsWith(streamsPath)) { + LOG.debug("Request path '{}' does not start with '{}'", path, streamsPath); return notFound(resp); } - final var stripped = path.substring(STREAMS_PREFIX.length()); + final var stripped = path.substring(streamsPath.length()); final int slash = stripped.indexOf('/'); if (slash < 0) { LOG.debug("Request path '{}' does not contain encoding", path); diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketInitializer.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketInitializer.java index 1f11a7e2c0..89052d64dd 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketInitializer.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketInitializer.java @@ -26,9 +26,9 @@ final class WebSocketInitializer extends WebSocketServlet { private final transient WebSocketFactory creator; private final int idleTimeoutMillis; - WebSocketInitializer(final RestconfStream.Registry streamRegistry, final PingExecutor pingExecutor, - final StreamsConfiguration configuration) { - creator = new WebSocketFactory(streamRegistry, pingExecutor, + WebSocketInitializer(final String restconf, final RestconfStream.Registry streamRegistry, + final PingExecutor pingExecutor, final StreamsConfiguration configuration) { + creator = new WebSocketFactory(restconf, streamRegistry, pingExecutor, configuration.maximumFragmentLength(), configuration.heartbeatInterval()); idleTimeoutMillis = configuration.idleTimeout(); } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProvider.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProvider.java index be29bdf7fe..d30a9d8c56 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProvider.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProvider.java @@ -13,10 +13,12 @@ import javax.inject.Singleton; import org.opendaylight.netconf.yanglib.writer.YangLibrarySchemaSourceUrlProvider; import org.opendaylight.restconf.nb.jaxrs.JaxRsRestconf; import org.opendaylight.restconf.nb.rfc8040.URLConstants; +import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStreamServletFactory; 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.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; /** * Component composing schema source URL value on per YANG resource basis. @@ -29,18 +31,19 @@ import org.osgi.service.component.annotations.Component; @Singleton @Component public final class RestconfSchemaSourceUrlProvider implements YangLibrarySchemaSourceUrlProvider { + private final String modulesPath; + @Inject @Activate - public RestconfSchemaSourceUrlProvider() { - // Visible for injection + public RestconfSchemaSourceUrlProvider(@Reference final RestconfStreamServletFactory servletFactory) { + modulesPath = "/" + servletFactory.restconf() + "/" + URLConstants.MODULES_SUBPATH + "/"; } @Override public Optional getSchemaSourceUrl(final String moduleSetName, final String moduleName, final Revision revision) { if ("ODL_modules".equals(moduleSetName)) { - final var sb = new StringBuilder("/" + URLConstants.BASE_PATH + "/" + URLConstants.MODULES_SUBPATH + "/") - .append(moduleName); + final var sb = new StringBuilder(modulesPath).append(moduleName); if (revision != null) { sb.append("?" + URLConstants.MODULES_REVISION_QUERY + "=").append(revision); } diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactoryTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactoryTest.java index 4570d3da22..89b63b5f27 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactoryTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/WebSocketFactoryTest.java @@ -66,9 +66,9 @@ class WebSocketFactoryTest extends AbstractNotificationListenerTest { doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit(); final var streamRegistry = new MdsalRestconfStreamRegistry(dataBroker); - webSocketFactory = new WebSocketFactory(streamRegistry, pingExecutor, 5000, 2000); + webSocketFactory = new WebSocketFactory("restconf", streamRegistry, pingExecutor, 5000, 2000); - streamName = streamRegistry.createStream(URI.create("https://localhost:8181/rests"), + streamName = streamRegistry.createStream(URI.create("https://localhost:8181/restconf"), new DataTreeChangeSource(databindProvider, changeService, LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(TOASTER)), "description") @@ -78,7 +78,7 @@ class WebSocketFactoryTest extends AbstractNotificationListenerTest { @Test void createWebSocketSuccessfully() { - doReturn(URI.create("https://localhost:8181/rests/streams/xml/" + streamName)) + doReturn(URI.create("https://localhost:8181/restconf/streams/xml/" + streamName)) .when(upgradeRequest).getRequestURI(); assertInstanceOf(WebSocketSender.class, @@ -89,7 +89,7 @@ class WebSocketFactoryTest extends AbstractNotificationListenerTest { @Test void createWebSocketUnsuccessfully() { - doReturn(URI.create("https://localhost:8181/rests/streams/xml/" + streamName + "/toasterStatus")) + doReturn(URI.create("https://localhost:8181/restconf/streams/xml/" + streamName + "/toasterStatus")) .when(upgradeRequest).getRequestURI(); assertNull(webSocketFactory.createWebSocket(upgradeRequest, upgradeResponse)); diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProviderTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProviderTest.java index acc4303b80..51927aa69c 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProviderTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/RestconfSchemaSourceUrlProviderTest.java @@ -9,22 +9,39 @@ package org.opendaylight.restconf.server.mdsal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStreamServletFactory; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Uri; import org.opendaylight.yangtools.yang.common.Revision; +@ExtendWith(MockitoExtension.class) class RestconfSchemaSourceUrlProviderTest { + @Mock + private RestconfStreamServletFactory servletFactory; + + private RestconfSchemaSourceUrlProvider urlProvider; + + @BeforeEach + void beforeEach() { + doReturn("restconf").when(servletFactory).restconf(); + urlProvider = new RestconfSchemaSourceUrlProvider(servletFactory); + } + @Test @DisplayName("Unsupported module-set name.") void unsupportedModuleSet() { - final var urlProvider = new RestconfSchemaSourceUrlProvider(); final var result = urlProvider.getSchemaSourceUrl("some-module-set", "module", null); assertTrue(result.isEmpty()); } @@ -32,7 +49,6 @@ class RestconfSchemaSourceUrlProviderTest { @ParameterizedTest(name = "Supported module-set name. URL: {2}") @MethodSource void getSchemaSourceUrl(final String moduleName, final Revision revision, final Uri expected) { - final var urlProvider = new RestconfSchemaSourceUrlProvider(); final var result = urlProvider.getSchemaSourceUrl("ODL_modules", moduleName, revision); assertEquals(Optional.of(expected), result); } @@ -40,8 +56,8 @@ class RestconfSchemaSourceUrlProviderTest { private static List getSchemaSourceUrl() { return List.of( Arguments.of("odl-module", Revision.of("2023-02-23"), - new Uri("/rests/modules/odl-module?revision=2023-02-23")), - Arguments.of("module-no-revision", null, new Uri("/rests/modules/module-no-revision")) + new Uri("/restconf/modules/odl-module?revision=2023-02-23")), + Arguments.of("module-no-revision", null, new Uri("/restconf/modules/module-no-revision")) ); } } -- 2.36.6