<type>xml</type>
<classifier>features</classifier>
</dependency>
- <dependency>
- <groupId>org.opendaylight.controller</groupId>
- <artifactId>odl-controller-exp-netty-config</artifactId>
- <type>xml</type>
- <classifier>features</classifier>
- </dependency>
<dependency>
<groupId>org.opendaylight.netconf</groupId>
<artifactId>odl-restconf-common</artifactId>
<features xmlns="http://karaf.apache.org/xmlns/features/v1.4.0" name="odl-restconf-nb-${project.version}">
<feature name="odl-restconf-nb" version="${project.version}">
<feature version="[12,13)">odl-mdsal-model-rfc8072</feature>
- <feature version="[8,9)">odl-controller-exp-netty-config</feature>
<configfile finalname="etc/org.opendaylight.restconf.nb.rfc8040.cfg">
mvn:org.opendaylight.netconf/restconf-nb/${project.version}/cfg/config
</configfile>
<optional>true</optional>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jdt</groupId>
+ <artifactId>org.eclipse.jdt.annotation</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.framework</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component</artifactId>
+ </dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
<artifactId>rfc8040-ietf-restconf-monitoring</artifactId>
</dependency>
- <dependency>
- <groupId>org.opendaylight.controller</groupId>
- <artifactId>threadpool-config-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.opendaylight.controller</groupId>
- <artifactId>threadpool-config-impl</artifactId>
- </dependency>
-
<dependency>
<groupId>net.java.dev.stax-utils</groupId>
<artifactId>stax-utils</artifactId>
import com.google.common.annotations.Beta;
import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterConfiguration;
import org.opendaylight.aaa.filterchain.filters.CustomFilterAdapter;
import org.opendaylight.aaa.web.FilterDetails;
import org.opendaylight.aaa.web.WebContextSecurer;
import org.opendaylight.aaa.web.WebServer;
import org.opendaylight.aaa.web.servlet.ServletSupport;
-import org.opendaylight.controller.config.threadpool.util.NamingThreadPoolFactory;
-import org.opendaylight.controller.config.threadpool.util.ScheduledThreadPoolWrapper;
import org.opendaylight.mdsal.dom.api.DOMActionService;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.MdsalRestconfServer;
-import org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker;
-import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
-import org.opendaylight.restconf.nb.rfc8040.streams.WebSocketInitializer;
+import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStreamServletFactory;
import org.opendaylight.yangtools.concepts.Registration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
-import org.osgi.service.metatype.annotations.AttributeDefinition;
-import org.osgi.service.metatype.annotations.Designate;
-import org.osgi.service.metatype.annotations.ObjectClassDefinition;
/**
* Main entrypoint into RFC8040 northbound. Take care of wiring up all applications activating them through JAX-RS.
*/
@Beta
-@Component(service = { }, configurationPid = "org.opendaylight.restconf.nb.rfc8040")
-@Designate(ocd = JaxRsNorthbound.Configuration.class)
+@Component(service = { })
public final class JaxRsNorthbound implements AutoCloseable {
- @ObjectClassDefinition
- public @interface Configuration {
- @AttributeDefinition(min = "0", max = "" + StreamsConfiguration.MAXIMUM_FRAGMENT_LENGTH_LIMIT)
- int maximum$_$fragment$_$length() default 0;
- @AttributeDefinition(min = "0")
- int heartbeat$_$interval() default 10000;
- @AttributeDefinition(min = "1")
- int idle$_$timeout() default 30000;
- @AttributeDefinition(min = "1")
- String ping$_$executor$_$name$_$prefix() default "ping-executor";
- // FIXME: this is a misnomer: it specifies the core pool size, i.e. minimum thread count, the maximum is set to
- // Integer.MAX_VALUE, which is not what we want
- @AttributeDefinition(min = "0")
- int max$_$thread$_$count() default 1;
- @AttributeDefinition
- boolean use$_$sse() default true;
- }
-
private final Registration discoveryReg;
private final Registration restconfReg;
@Reference final DOMMountPointService mountPointService,
@Reference final DOMNotificationService notificationService, @Reference final DOMRpcService rpcService,
@Reference final DOMSchemaService schemaService, @Reference final DatabindProvider databindProvider,
- @Reference final MdsalRestconfServer server, final Configuration configuration) throws ServletException {
- this(webServer, webContextSecurer, servletSupport, filterAdapterConfiguration, actionService, dataBroker,
- mountPointService, notificationService, rpcService, schemaService, databindProvider, server,
- configuration.ping$_$executor$_$name$_$prefix(), configuration.max$_$thread$_$count(),
- new StreamsConfiguration(configuration.maximum$_$fragment$_$length(),
- configuration.idle$_$timeout(), configuration.heartbeat$_$interval(), configuration.use$_$sse()));
- }
-
- public JaxRsNorthbound(final WebServer webServer, final WebContextSecurer webContextSecurer,
- final ServletSupport servletSupport, final CustomFilterAdapterConfiguration filterAdapterConfiguration,
- final DOMActionService actionService, final DOMDataBroker dataBroker,
- final DOMMountPointService mountPointService, final DOMNotificationService notificationService,
- final DOMRpcService rpcService, final DOMSchemaService schemaService,
- final DatabindProvider databindProvider, final MdsalRestconfServer server, final String pingNamePrefix,
- final int pingMaxThreadCount, final StreamsConfiguration streamsConfiguration) throws ServletException {
- final var scheduledThreadPool = new ScheduledThreadPoolWrapper(pingMaxThreadCount,
- new NamingThreadPoolFactory(pingNamePrefix));
-
- final ListenersBroker listenersBroker;
- final HttpServlet streamServlet;
- if (streamsConfiguration.useSSE()) {
- listenersBroker = new ListenersBroker.ServerSentEvents(dataBroker, notificationService, mountPointService);
- streamServlet = servletSupport.createHttpServletBuilder(
- new ServerSentEventsApplication(scheduledThreadPool, listenersBroker, streamsConfiguration))
- .build();
- } else {
- listenersBroker = new ListenersBroker.WebSockets(dataBroker, notificationService, mountPointService);
- streamServlet = new WebSocketInitializer(scheduledThreadPool, listenersBroker, streamsConfiguration);
- }
-
+ @Reference final MdsalRestconfServer server, @Reference final RestconfStreamServletFactory servletFactory)
+ throws ServletException {
final var restconfBuilder = WebContext.builder()
.name("RFC8040 RESTCONF")
.contextPath("/" + URLConstants.BASE_PATH)
.addUrlPattern("/*")
.servlet(servletSupport.createHttpServletBuilder(
new RestconfApplication(databindProvider, server, mountPointService, dataBroker, actionService,
- notificationService, schemaService, listenersBroker))
+ notificationService, schemaService))
.build())
.asyncSupported(true)
.build())
.addServlet(ServletDetails.builder()
.addUrlPattern("/" + URLConstants.STREAMS_SUBPATH + "/*")
- .servlet(streamServlet)
+ .servlet(servletFactory.newStreamServlet())
.name("notificationServlet")
.asyncSupported(true)
.build())
--- /dev/null
+/*
+ * 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;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Map;
+import org.opendaylight.restconf.nb.rfc8040.streams.DefaultPingExecutor;
+import org.opendaylight.restconf.nb.rfc8040.streams.DefaultRestconfStreamServletFactory;
+import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
+import org.opendaylight.restconf.server.mdsal.MdsalRestconfStreamRegistry;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.component.ComponentFactory;
+import org.osgi.service.component.ComponentInstance;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Component managing global RESTCONF northbound configuration.
+ */
+@Component(service = { }, configurationPid = "org.opendaylight.restconf.nb.rfc8040")
+@Designate(ocd = OSGiNorthbound.Configuration.class)
+public final class OSGiNorthbound {
+ @ObjectClassDefinition
+ public @interface Configuration {
+ @AttributeDefinition(min = "0", max = "" + StreamsConfiguration.MAXIMUM_FRAGMENT_LENGTH_LIMIT)
+ int maximum$_$fragment$_$length() default 0;
+ @AttributeDefinition(min = "0")
+ int heartbeat$_$interval() default 10000;
+ @AttributeDefinition(min = "1")
+ int idle$_$timeout() default 30000;
+ @AttributeDefinition(min = "1")
+ String ping$_$executor$_$name$_$prefix() default DefaultPingExecutor.DEFAULT_NAME_PREFIX;
+ // FIXME: this is a misnomer: it specifies the core pool size, i.e. minimum thread count, the maximum is set to
+ // Integer.MAX_VALUE, which is not what we want
+ @AttributeDefinition(min = "0")
+ int max$_$thread$_$count() default DefaultPingExecutor.DEFAULT_CORE_POOL_SIZE;
+ @AttributeDefinition
+ boolean use$_$sse() default true;
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(OSGiNorthbound.class);
+
+ private final ComponentFactory<MdsalRestconfStreamRegistry> registryFactory;
+ private final ComponentFactory<DefaultRestconfStreamServletFactory> servletFactoryFactory;
+
+ private ComponentInstance<MdsalRestconfStreamRegistry> registry;
+ private boolean useSSE;
+
+ private ComponentInstance<DefaultRestconfStreamServletFactory> servletFactory;
+ private Map<String, ?> servletProps;
+
+ @Activate
+ public OSGiNorthbound(
+ @Reference(target = "(component.factory=" + DefaultRestconfStreamServletFactory.FACTORY_NAME + ")")
+ final ComponentFactory<DefaultRestconfStreamServletFactory> servletFactoryFactory,
+ @Reference(target = "(component.factory=" + MdsalRestconfStreamRegistry.FACTORY_NAME + ")")
+ final ComponentFactory<MdsalRestconfStreamRegistry> registryFactory, final Configuration configuration) {
+ this.registryFactory = requireNonNull(registryFactory);
+ this.servletFactoryFactory = requireNonNull(servletFactoryFactory);
+
+ useSSE = configuration.use$_$sse();
+ registry = registryFactory.newInstance(FrameworkUtil.asDictionary(MdsalRestconfStreamRegistry.props(useSSE)));
+
+ servletProps = DefaultRestconfStreamServletFactory.props(registry.getInstance(), useSSE,
+ new StreamsConfiguration(configuration.maximum$_$fragment$_$length(),
+ configuration.idle$_$timeout(), configuration.heartbeat$_$interval()),
+ configuration.ping$_$executor$_$name$_$prefix(), configuration.max$_$thread$_$count());
+ servletFactory = servletFactoryFactory.newInstance(FrameworkUtil.asDictionary(servletProps));
+
+ LOG.info("Global RESTCONF northbound pools started");
+ }
+
+ @Modified
+ void modified(final Configuration configuration) {
+ final var newUseSSE = configuration.use$_$sse();
+ if (newUseSSE != useSSE) {
+ useSSE = newUseSSE;
+ registry.dispose();
+ registry = registryFactory.newInstance(FrameworkUtil.asDictionary(
+ MdsalRestconfStreamRegistry.props(useSSE)));
+ LOG.debug("ListenersBroker restarted with {}", newUseSSE ? "SSE" : "Websockets");
+ }
+
+ final var newServletProps = DefaultRestconfStreamServletFactory.props(registry.getInstance(), useSSE,
+ new StreamsConfiguration(configuration.maximum$_$fragment$_$length(),
+ configuration.idle$_$timeout(), configuration.heartbeat$_$interval()),
+ configuration.ping$_$executor$_$name$_$prefix(), configuration.max$_$thread$_$count());
+ if (!newServletProps.equals(servletProps)) {
+ servletProps = newServletProps;
+ servletFactory.dispose();
+ servletFactory = servletFactoryFactory.newInstance(FrameworkUtil.asDictionary(servletProps));
+ LOG.debug("RestconfStreamServletFactory restarted with {}", servletProps);
+ }
+
+ LOG.debug("Applied {}", configuration);
+ }
+
+ @Deactivate
+ void deactivate() {
+ servletFactory.dispose();
+ servletFactory = null;
+ registry.dispose();
+ registry = null;
+ LOG.info("Global RESTCONF northbound pools stopped");
+ }
+}
import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfInvokeOperationsServiceImpl;
import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfOperationsServiceImpl;
import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfSchemaServiceImpl;
-import org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker;
final class RestconfApplication extends Application {
private final Set<Object> singletons;
RestconfApplication(final DatabindProvider databindProvider, final MdsalRestconfServer server,
final DOMMountPointService mountPointService, final DOMDataBroker dataBroker,
final DOMActionService actionService, final DOMNotificationService notificationService,
- final DOMSchemaService domSchemaService, final ListenersBroker listenersBroker) {
+ final DOMSchemaService domSchemaService) {
singletons = Set.of(
new RestconfDocumentedExceptionMapper(databindProvider),
new RestconfDataServiceImpl(databindProvider, server, actionService),
- new RestconfInvokeOperationsServiceImpl(databindProvider, server, listenersBroker),
- new RestconfOperationsServiceImpl(databindProvider, server),
+ new RestconfInvokeOperationsServiceImpl(server),
+ new RestconfOperationsServiceImpl(server),
new RestconfSchemaServiceImpl(domSchemaService, mountPointService),
new RestconfImpl(databindProvider));
}
+++ /dev/null
-/*
- * Copyright (c) 2020 Lumina Networks, Inc. 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;
-
-import java.util.Set;
-import javax.ws.rs.core.Application;
-import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
-import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfDataStreamServiceImpl;
-import org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker;
-import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
-
-/**
- * JAX-RS binding for Server-Sent Events.
- */
-final class ServerSentEventsApplication extends Application {
- private final RestconfDataStreamServiceImpl singleton;
-
- ServerSentEventsApplication(final ScheduledThreadPool scheduledThreadPool, final ListenersBroker listenersBroker,
- final StreamsConfiguration configuration) {
- singleton = new RestconfDataStreamServiceImpl(scheduledThreadPool, listenersBroker, configuration);
- }
-
- @Override
- public Set<Object> getSingletons() {
- return Set.of(singleton);
- }
-}
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
+import java.net.URI;
+import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMRpcService;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
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.RestconfServer;
+import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
+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.data.api.schema.ContainerNode;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
/**
* A RESTCONF server implemented on top of MD-SAL.
*/
-// FIXME: factor out the 'RestconfServer' interface once we're ready
// FIXME: this should live in 'org.opendaylight.restconf.server.mdsal' package
@Singleton
-@Component(service = MdsalRestconfServer.class)
-public final class MdsalRestconfServer {
+@Component(service = { MdsalRestconfServer.class, RestconfServer.class })
+public final class MdsalRestconfServer implements RestconfServer {
private static final Logger LOG = LoggerFactory.getLogger(MdsalRestconfServer.class);
private static final VarHandle LOCAL_STRATEGY;
}
}
+ private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
private final @NonNull DOMMountPointService mountPointService;
+ private final @NonNull DatabindProvider databindProvider;
private final @NonNull DOMDataBroker dataBroker;
private final @Nullable DOMRpcService rpcService;
@Inject
@Activate
- public MdsalRestconfServer(@Reference final DOMDataBroker dataBroker, @Reference final DOMRpcService rpcService,
- @Reference final DOMMountPointService mountPointService) {
+ public MdsalRestconfServer(@Reference final DatabindProvider databindProvider,
+ @Reference final DOMDataBroker dataBroker, @Reference final DOMRpcService rpcService,
+ @Reference final DOMMountPointService mountPointService,
+ @Reference final List<RpcImplementation> localRpcs) {
+ this.databindProvider = requireNonNull(databindProvider);
this.dataBroker = requireNonNull(dataBroker);
this.rpcService = requireNonNull(rpcService);
this.mountPointService = requireNonNull(mountPointService);
+ this.localRpcs = Maps.uniqueIndex(localRpcs, RpcImplementation::qname);
}
+ public MdsalRestconfServer(final DatabindProvider databind, final DOMDataBroker dataBroker,
+ final DOMRpcService rpcService, final DOMMountPointService mountPointService,
+ final RpcImplementation... localRpcs) {
+ this(databind, dataBroker, rpcService, mountPointService, List.of(localRpcs));
+ }
+
+ @NonNull InstanceIdentifierContext bindRequestPath(final String identifier) {
+ return bindRequestPath(databindProvider.currentContext(), identifier);
+ }
+
+ @Deprecated
@NonNull InstanceIdentifierContext bindRequestPath(final DatabindContext databind, final String identifier) {
// FIXME: go through ApiPath first. That part should eventually live in callers
// FIXME: DatabindContext looks like it should be internal
mountPointService));
}
- @SuppressWarnings("static-method")
- @NonNull InstanceIdentifierContext bindRequestRoot(final DatabindContext databind) {
- return InstanceIdentifierContext.ofLocalRoot(databind.modelContext());
+ @Override
+ public RestconfFuture<OperationOutput> invokeRpc(final URI restconfURI, final String apiPath,
+ final OperationInputBody body) {
+ final var currentContext = databindProvider.currentContext();
+ final var reqPath = bindRequestPath(currentContext, apiPath);
+ final var inference = reqPath.inference();
+ final ContainerNode input;
+ try {
+ input = body.toContainerNode(inference);
+ } catch (IOException e) {
+ LOG.debug("Error reading input", e);
+ return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
+ }
+
+ return getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint())
+ .invokeRpc(restconfURI, reqPath.getSchemaNode().getQName(),
+ new OperationInput(currentContext, inference, input));
+ }
+
+ @NonNull InstanceIdentifierContext bindRequestRoot() {
+ return InstanceIdentifierContext.ofLocalRoot(databindProvider.currentContext().modelContext());
}
@VisibleForTesting
return local;
}
- final var created = new MdsalRestconfStrategy(modelContext, dataBroker, rpcService);
+ final var created = new MdsalRestconfStrategy(modelContext, dataBroker, rpcService, localRpcs);
LOCAL_STRATEGY.setRelease(this, created);
return created;
}
})
public Response readData(@Context final UriInfo uriInfo) {
final var readParams = QueryParams.newReadDataParams(uriInfo);
- return readData(server.bindRequestRoot(databindProvider.currentContext()), readParams);
+ return readData(server.bindRequestRoot(), readParams);
}
/**
public Response readData(@Encoded @PathParam("identifier") final String identifier,
@Context final UriInfo uriInfo) {
final var readParams = QueryParams.newReadDataParams(uriInfo);
- return readData(server.bindRequestPath(databindProvider.currentContext(), identifier), readParams);
+ return readData(server.bindRequestPath(identifier), readParams);
}
private Response readData(final InstanceIdentifierContext reqPath, final ReadDataParams readParams) {
}
if (node == null) {
throw new RestconfDocumentedException(
- "Request could not be completed because the relevant data model content does not exist",
- ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
+ "Request could not be completed because the relevant data model content does not exist",
+ ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
}
return switch (readParams.content()) {
private void putData(final @Nullable String identifier, final UriInfo uriInfo, final ResourceBody body,
final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
+ final var reqPath = server.bindRequestPath(identifier);
final var insert = QueryParams.parseInsert(reqPath.getSchemaContext(), uriInfo);
final var req = bindResourceRequest(reqPath, body);
})
public void postDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
+ final var reqPath = server.bindRequestPath(identifier);
if (reqPath.getSchemaNode() instanceof ActionDefinition) {
try (var jsonBody = new JsonOperationInputBody(body)) {
invokeAction(reqPath, jsonBody, ar);
})
public void postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
+ final var reqPath = server.bindRequestPath(identifier);
if (reqPath.getSchemaNode() instanceof ActionDefinition) {
try (var xmlBody = new XmlOperationInputBody(body)) {
invokeAction(reqPath, xmlBody, ar);
@Path("/data/{identifier:.+}")
public void deleteData(@Encoded @PathParam("identifier") final String identifier,
@Suspended final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
+ final var reqPath = server.bindRequestPath(identifier);
final var strategy = server.getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
strategy.delete(reqPath.getInstanceIdentifier()).addCallback(new JaxRsRestconfCallback<>(ar) {
* @param ar {@link AsyncResponse} which needs to be completed
*/
private void plainPatchData(final ResourceBody body, final AsyncResponse ar) {
- plainPatchData(server.bindRequestRoot(databindProvider.currentContext()), body, ar);
+ plainPatchData(server.bindRequestRoot(), body, ar);
}
/**
* @param ar {@link AsyncResponse} which needs to be completed
*/
private void plainPatchData(final String identifier, final ResourceBody body, final AsyncResponse ar) {
- plainPatchData(server.bindRequestPath(databindProvider.currentContext(), identifier), body, ar);
+ plainPatchData(server.bindRequestPath(identifier), body, ar);
}
/**
}
private void yangPatchData(final @NonNull PatchBody body, final AsyncResponse ar) {
- final var context = databindProvider.currentContext().modelContext();
+ final var context = server.bindRequestRoot().getSchemaContext();
yangPatchData(context, parsePatchBody(context, YangInstanceIdentifier.of(), body), null, ar);
}
private void yangPatchData(final String identifier, final @NonNull PatchBody body,
final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
+ final var reqPath = server.bindRequestPath(identifier);
final var modelContext = reqPath.getSchemaContext();
yangPatchData(modelContext, parsePatchBody(modelContext, reqPath.getInstanceIdentifier(), body),
reqPath.getMountPoint(), ar);
private static Status getStatusCode(final PatchStatusContext result) {
if (result.ok()) {
return Status.OK;
+ } else if (result.globalErrors() == null || result.globalErrors().isEmpty()) {
+ return result.editCollection().stream()
+ .filter(patchStatus -> !patchStatus.isOk() && !patchStatus.getEditErrors().isEmpty())
+ .findFirst()
+ .map(PatchStatusEntity::getEditErrors)
+ .flatMap(errors -> errors.stream().findFirst())
+ .map(error -> ErrorTags.statusOf(error.getErrorTag()))
+ .orElse(Status.INTERNAL_SERVER_ERROR);
} else {
- if (result.globalErrors() == null || result.globalErrors().isEmpty()) {
- return result.editCollection().stream()
- .filter(patchStatus -> !patchStatus.isOk() && !patchStatus.getEditErrors().isEmpty())
- .findFirst()
- .map(PatchStatusEntity::getEditErrors)
- .flatMap(errors -> errors.stream().findFirst())
- .map(error -> ErrorTags.statusOf(error.getErrorTag()))
- .orElse(Status.INTERNAL_SERVER_ERROR);
- } else {
- final var error = result.globalErrors().iterator().next();
- return ErrorTags.statusOf(error.getErrorTag());
- }
+ final var error = result.globalErrors().iterator().next();
+ return ErrorTags.statusOf(error.getErrorTag());
}
}
import static java.util.Objects.requireNonNull;
-import java.io.IOException;
import java.io.InputStream;
-import java.util.Optional;
import javax.ws.rs.Consumes;
import javax.ws.rs.Encoded;
import javax.ws.rs.POST;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.common.errors.RestconfFuture;
import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
-import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
-import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
-import org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.device.notification.rev221106.SubscribeDeviceNotification;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscription;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateNotificationStream;
-import org.opendaylight.yangtools.yang.common.ErrorTag;
-import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.opendaylight.restconf.server.spi.OperationOutput;
/**
* An operation resource represents a protocol operation defined with the YANG {@code rpc} statement. It is invoked
*/
@Path("/")
public final class RestconfInvokeOperationsServiceImpl {
- private static final Logger LOG = LoggerFactory.getLogger(RestconfInvokeOperationsServiceImpl.class);
-
- private final DatabindProvider databindProvider;
private final MdsalRestconfServer server;
- private final ListenersBroker listenersBroker;
- public RestconfInvokeOperationsServiceImpl(final DatabindProvider databindProvider,
- final MdsalRestconfServer server, final ListenersBroker listenersBroker) {
- this.databindProvider = requireNonNull(databindProvider);
+ public RestconfInvokeOperationsServiceImpl(final MdsalRestconfServer server) {
this.server = requireNonNull(server);
- this.listenersBroker = requireNonNull(listenersBroker);
}
/**
private void invokeRpc(final String identifier, final UriInfo uriInfo, final AsyncResponse ar,
final OperationInputBody body) {
- final var databind = databindProvider.currentContext();
- final var reqPath = server.bindRequestPath(databind, identifier);
-
- final ContainerNode input;
- try {
- input = body.toContainerNode(reqPath.inference());
- } catch (IOException e) {
- LOG.debug("Error reading input", e);
- throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
- ErrorTag.MALFORMED_MESSAGE, e);
- }
-
- hackInvokeRpc(databind, reqPath, uriInfo, input).addCallback(new JaxRsRestconfCallback<>(ar) {
- @Override
- Response transform(final Optional<ContainerNode> result) {
- return result
- .filter(output -> !output.isEmpty())
- .map(output -> Response.ok().entity(new NormalizedNodePayload(reqPath.inference(), output)).build())
- .orElseGet(() -> Response.noContent().build());
- }
- });
- }
-
- private RestconfFuture<Optional<ContainerNode>> hackInvokeRpc(final DatabindContext localDatabind,
- final InstanceIdentifierContext reqPath, final UriInfo uriInfo, final ContainerNode input) {
- // RPC type
- final var type = reqPath.getSchemaNode().getQName();
- final var mountPoint = reqPath.getMountPoint();
- if (mountPoint == null) {
- final var baseURI = uriInfo.getBaseUri();
- // Hacked-up integration of streams
- if (CreateDataChangeEventSubscription.QNAME.equals(type)) {
- return listenersBroker.createDataChangeNotifiStream(databindProvider, baseURI, input,
- localDatabind.modelContext());
- } else if (CreateNotificationStream.QNAME.equals(type)) {
- return listenersBroker.createNotificationStream(databindProvider, baseURI, input,
- localDatabind.modelContext());
- } else if (SubscribeDeviceNotification.QNAME.equals(type)) {
- return listenersBroker.createDeviceNotificationStream(baseURI, input, localDatabind.modelContext());
- }
- }
-
- return server.getRestconfStrategy(reqPath.getSchemaContext(), mountPoint).invokeRpc(type, input);
+ server.invokeRpc(uriInfo.getBaseUri(), identifier, body)
+ .addCallback(new JaxRsRestconfCallback<OperationOutput>(ar) {
+ @Override
+ 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();
+ }
+ });
}
}
*/
@Path("/")
public final class RestconfOperationsServiceImpl {
- private final DatabindProvider databindProvider;
private final MdsalRestconfServer server;
/**
* Set {@link DatabindProvider} for getting actual {@link EffectiveModelContext}.
*
- * @param databindProvider a {@link DatabindProvider}
* @param server a {@link MdsalRestconfServer}
*/
- public RestconfOperationsServiceImpl(final DatabindProvider databindProvider, final MdsalRestconfServer server) {
- this.databindProvider = requireNonNull(databindProvider);
+ public RestconfOperationsServiceImpl(final MdsalRestconfServer server) {
this.server = requireNonNull(server);
}
@Path("/operations")
@Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
public String getOperationsJSON() {
- return OperationsContent.JSON.bodyFor(
- server.bindRequestRoot(databindProvider.currentContext()).inference());
+ return OperationsContent.JSON.bodyFor(server.bindRequestRoot().inference());
}
/**
@Path("/operations/{identifier:.+}")
@Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
public String getOperationJSON(@PathParam("identifier") final String identifier) {
- return OperationsContent.JSON.bodyFor(
- server.bindRequestPath(databindProvider.currentContext(), identifier).inference());
+ return OperationsContent.JSON.bodyFor(server.bindRequestPath(identifier).inference());
}
/**
@Path("/operations")
@Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public String getOperationsXML() {
- return OperationsContent.XML.bodyFor(
- server.bindRequestRoot(databindProvider.currentContext()).inference());
+ return OperationsContent.XML.bodyFor(server.bindRequestRoot().inference());
}
/**
@Path("/operations/{identifier:.+}")
@Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public String getOperationXML(@PathParam("identifier") final String identifier) {
- return OperationsContent.XML.bodyFor(
- server.bindRequestPath(databindProvider.currentContext(), identifier).inference());
+ return OperationsContent.XML.bodyFor(server.bindRequestPath(identifier).inference());
}
}
import static java.util.Objects.requireNonNull;
import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION;
+import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
import org.opendaylight.yangtools.yang.common.Empty;
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.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
private final DOMDataBroker dataBroker;
public MdsalRestconfStrategy(final EffectiveModelContext modelContext, final DOMDataBroker dataBroker,
- final @Nullable DOMRpcService rpcService) {
- super(modelContext, rpcService);
+ final @Nullable DOMRpcService rpcService, final ImmutableMap<QName, RpcImplementation> localRpcs) {
+ super(modelContext, localRpcs, rpcService);
this.dataBroker = requireNonNull(dataBroker);
}
+ public MdsalRestconfStrategy(final EffectiveModelContext modelContext, final DOMDataBroker dataBroker,
+ final @Nullable DOMRpcService rpcService) {
+ this(modelContext, dataBroker, rpcService, ImmutableMap.of());
+ }
+
@Override
RestconfTransaction prepareWriteExecution() {
return new MdsalRestconfTransaction(modelContext(), dataBroker);
import static java.util.Objects.requireNonNull;
+import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
public NetconfRestconfStrategy(final EffectiveModelContext modelContext,
final NetconfDataTreeService netconfService, final @Nullable DOMRpcService rpcService) {
- super(modelContext, rpcService);
+ super(modelContext, ImmutableMap.of(), rpcService);
this.netconfService = requireNonNull(netconfService);
}
import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
+import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.common.patch.PatchStatusEntity;
import org.opendaylight.restconf.nb.rfc8040.Insert;
+import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.with.defaults.rev110601.WithDefaultsMode;
import org.opendaylight.yangtools.yang.common.Empty;
import org.opendaylight.yangtools.yang.common.ErrorTag;
private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
private final @NonNull EffectiveModelContext modelContext;
- private final @Nullable DOMRpcService rpcService;
+ private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
+ private final DOMRpcService rpcService;
- RestconfStrategy(final EffectiveModelContext modelContext, final @Nullable DOMRpcService rpcService) {
+ RestconfStrategy(final EffectiveModelContext modelContext, final ImmutableMap<QName, RpcImplementation> localRpcs,
+ final @Nullable DOMRpcService rpcService) {
this.modelContext = requireNonNull(modelContext);
+ this.localRpcs = requireNonNull(localRpcs);
this.rpcService = rpcService;
}
y -> builder.addChild((T) prepareData(y.getValue(), stateMap.get(y.getKey()))));
}
- public @NonNull RestconfFuture<Optional<ContainerNode>> invokeRpc(final QName type, final ContainerNode input) {
- final var ret = new SettableRestconfFuture<Optional<ContainerNode>>();
-
- final var local = rpcService;
+ public @NonNull RestconfFuture<OperationOutput> invokeRpc(final URI restconfURI, final QName type,
+ final OperationInput input) {
+ final var local = localRpcs.get(type);
if (local != null) {
- Futures.addCallback(local.invokeRpc(requireNonNull(type), requireNonNull(input)),
- new FutureCallback<DOMRpcResult>() {
- @Override
- public void onSuccess(final DOMRpcResult response) {
- final var errors = response.errors();
- if (errors.isEmpty()) {
- ret.set(Optional.ofNullable(response.value()));
- } else {
- LOG.debug("RPC invocation reported {}", response.errors());
- ret.setFailure(new RestconfDocumentedException("RPC implementation reported errors", null,
- response.errors()));
- }
- }
-
- @Override
- public void onFailure(final Throwable cause) {
- LOG.debug("RPC invocation failed, cause");
- if (cause instanceof RestconfDocumentedException ex) {
- ret.setFailure(ex);
- } else {
- // TODO: YangNetconfErrorAware if we ever get into a broader invocation scope
- ret.setFailure(new RestconfDocumentedException(cause,
- new RestconfError(ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage())));
- }
- }
- }, MoreExecutors.directExecutor());
- } else {
+ return local.invoke(restconfURI, input);
+ }
+ if (rpcService == null) {
LOG.debug("RPC invocation is not available");
- ret.setFailure(new RestconfDocumentedException("RPC invocation is not available",
+ return RestconfFuture.failed(new RestconfDocumentedException("RPC invocation is not available",
ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED));
}
+
+ final var ret = new SettableRestconfFuture<OperationOutput>();
+ Futures.addCallback(rpcService.invokeRpc(requireNonNull(type), input.input()),
+ new FutureCallback<DOMRpcResult>() {
+ @Override
+ public void onSuccess(final DOMRpcResult response) {
+ final var errors = response.errors();
+ if (errors.isEmpty()) {
+ ret.set(input.newOperationOutput(response.value()));
+ } else {
+ LOG.debug("RPC invocation reported {}", response.errors());
+ ret.setFailure(new RestconfDocumentedException("RPC implementation reported errors", null,
+ response.errors()));
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ LOG.debug("RPC invocation failed, cause");
+ if (cause instanceof RestconfDocumentedException ex) {
+ ret.setFailure(ex);
+ } else {
+ // TODO: YangNetconfErrorAware if we ever get into a broader invocation scope
+ ret.setFailure(new RestconfDocumentedException(cause,
+ new RestconfError(ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage())));
+ }
+ }
+ }, MoreExecutors.directExecutor());
return ret;
}
}
--- /dev/null
+/*
+ * 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.streams;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.opendaylight.yangtools.concepts.AbstractRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
+
+@Singleton
+public final class DefaultPingExecutor implements PingExecutor, AutoCloseable {
+ private static final class Process extends AbstractRegistration implements Runnable {
+ private final Runnable task;
+ private final ScheduledFuture<?> future;
+
+ Process(final Runnable task, final ScheduledThreadPoolExecutor threadPool, final long delay,
+ final TimeUnit timeUnit) {
+ this.task = requireNonNull(task);
+ future = threadPool.scheduleWithFixedDelay(task, delay, delay, timeUnit);
+ }
+
+ @Override
+ protected void removeRegistration() {
+ future.cancel(false);
+ }
+
+ @Override
+ public void run() {
+ if (notClosed()) {
+ task.run();
+ }
+ }
+ }
+
+ public static final String DEFAULT_NAME_PREFIX = "ping-executor";
+ public static final int DEFAULT_CORE_POOL_SIZE = 1;
+
+ // FIXME: Java 21: just use thread-per-task executor with virtual threads
+ private final ScheduledThreadPoolExecutor threadPool;
+
+ public DefaultPingExecutor(final String namePrefix, final int corePoolSize) {
+ final var counter = new AtomicLong();
+ final var group = new ThreadGroup(requireNonNull(namePrefix));
+ threadPool = new ScheduledThreadPoolExecutor(corePoolSize,
+ target -> new Thread(group, target, namePrefix + '-' + counter.incrementAndGet()));
+ }
+
+ @Inject
+ public DefaultPingExecutor() {
+ this(DEFAULT_NAME_PREFIX, DEFAULT_CORE_POOL_SIZE);
+ }
+
+ @Override
+ public Registration startPingProcess(final Runnable task, final long delay, final TimeUnit timeUnit) {
+ return new Process(task, threadPool, delay, timeUnit);
+ }
+
+ @Override
+ @PreDestroy
+ public void close() {
+ threadPool.shutdown();
+ }
+}
--- /dev/null
+/*
+ * 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.streams;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Map;
+import javax.servlet.http.HttpServlet;
+import org.opendaylight.aaa.web.servlet.ServletSupport;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Auxiliary interface for instantiating JAX-RS streams.
+ */
+@Component(factory = DefaultRestconfStreamServletFactory.FACTORY_NAME, service = RestconfStreamServletFactory.class)
+public final class DefaultRestconfStreamServletFactory implements RestconfStreamServletFactory, AutoCloseable {
+ public static final String FACTORY_NAME =
+ "org.opendaylight.restconf.nb.rfc8040.streams.RestconfStreamServletFactory";
+
+ private static final String PROP_STREAM_REGISTRY = ".streamRegistry";
+ private static final String PROP_NAME_PREFIX = ".namePrefix";
+ 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 final RestconfStream.Registry streamRegistry;
+ private final ServletSupport servletSupport;
+
+ private final DefaultPingExecutor pingExecutor;
+ private final StreamsConfiguration streamsConfiguration;
+ private final boolean useWebsockets;
+
+ public DefaultRestconfStreamServletFactory(final ServletSupport servletSupport,
+ final RestconfStream.Registry streamRegistry, final StreamsConfiguration streamsConfiguration,
+ final String namePrefix, final int corePoolSize, final boolean useWebsockets) {
+ this.servletSupport = requireNonNull(servletSupport);
+ this.streamRegistry = requireNonNull(streamRegistry);
+ this.streamsConfiguration = requireNonNull(streamsConfiguration);
+ this.useWebsockets = useWebsockets;
+ pingExecutor = new DefaultPingExecutor(namePrefix, corePoolSize);
+ }
+
+ @Activate
+ public DefaultRestconfStreamServletFactory(@Reference final ServletSupport servletSupport,
+ final Map<String, ?> props) {
+ this(servletSupport, (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 HttpServlet newStreamServlet() {
+ return useWebsockets ? new WebSocketInitializer(streamRegistry, pingExecutor, streamsConfiguration)
+ : servletSupport.createHttpServletBuilder(
+ new SSEApplication(streamRegistry, pingExecutor, streamsConfiguration))
+ .build();
+ }
+
+ @Override
+ @Deactivate
+ public void close() {
+ pingExecutor.close();
+ }
+
+ public static Map<String, ?> props(final RestconfStream.Registry streamRegistry, final boolean useSSE,
+ final StreamsConfiguration streamsConfiguration, final String namePrefix, final int corePoolSize) {
+ return Map.of(
+ PROP_STREAM_REGISTRY, streamRegistry,
+ PROP_USE_WEBSOCKETS, !useSSE,
+ PROP_STREAMS_CONFIGURATION, streamsConfiguration,
+ PROP_NAME_PREFIX, namePrefix,
+ PROP_CORE_POOL_SIZE, corePoolSize);
+ }
+}
+++ /dev/null
-/*
- * Copyright © 2019 FRINX s.r.o. 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.streams;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.MoreExecutors;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.mdsal.common.api.CommitInfo;
-import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
-import org.opendaylight.mdsal.dom.api.DOMDataBroker;
-import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.mdsal.dom.api.DOMNotificationService;
-import org.opendaylight.mdsal.dom.api.DOMRpcResult;
-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.URLConstants;
-import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Source;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.RestconfState;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.Streams;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.Stream;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.stream.Access;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.device.notification.rev221106.SubscribeDeviceNotificationInput;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.device.notification.rev221106.SubscribeDeviceNotificationOutput;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscriptionInput;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscriptionOutput;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateNotificationStreamInput;
-import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev231103.CreateDataChangeEventSubscriptionInput1;
-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.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
-import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
-import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
-import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
-import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
-import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
-import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * This singleton class is responsible for creation, removal and searching for {@link DataTreeChangeSource} or
- * {@link NotificationSource} listeners.
- */
-// FIXME: furthermore, this should be tied to ietf-restconf-monitoring, as the Strings used in its maps are stream
-// names. We essentially need a component which deals with allocation of stream names and their lifecycle and
-// the contents of /restconf-state/streams.
-public abstract sealed class ListenersBroker {
- /**
- * A ListenersBroker working with Server-Sent Events.
- */
- public static final class ServerSentEvents extends ListenersBroker {
- public ServerSentEvents(final DOMDataBroker dataBroker, final DOMNotificationService notificationService,
- final DOMMountPointService mountPointService) {
- super(dataBroker, notificationService, mountPointService);
- }
- }
-
- /**
- * A ListenersBroker working with WebSockets.
- */
- public static final class WebSockets extends ListenersBroker {
- public WebSockets(final DOMDataBroker dataBroker, final DOMNotificationService notificationService,
- final DOMMountPointService mountPointService) {
- super(dataBroker, notificationService, mountPointService);
- }
-
- @Override
- String streamsScheme(final URI baseURI) {
- return switch (super.streamsScheme(baseURI)) {
- // Secured HTTP goes to Secured WebSockets
- case "https" -> "wss";
- // Unsecured HTTP and others go to unsecured WebSockets
- default -> "ws";
- };
- }
- }
-
- private static final Logger LOG = LoggerFactory.getLogger(ListenersBroker.class);
- private static final YangInstanceIdentifier RESTCONF_STATE_STREAMS = YangInstanceIdentifier.of(
- NodeIdentifier.create(RestconfState.QNAME),
- NodeIdentifier.create(Streams.QNAME),
- NodeIdentifier.create(Stream.QNAME));
-
- @VisibleForTesting
- static final QName NAME_QNAME = QName.create(Stream.QNAME, "name").intern();
- @VisibleForTesting
- static final QName DESCRIPTION_QNAME = QName.create(Stream.QNAME, "description").intern();
- @VisibleForTesting
- static final QName ENCODING_QNAME = QName.create(Stream.QNAME, "encoding").intern();
- @VisibleForTesting
- static final QName LOCATION_QNAME = QName.create(Stream.QNAME, "location").intern();
-
- private static final NodeIdentifier DATASTORE_NODEID = NodeIdentifier.create(
- QName.create(CreateDataChangeEventSubscriptionInput1.QNAME, "datastore").intern());
- private static final NodeIdentifier DEVICE_NOTIFICATION_PATH_NODEID =
- NodeIdentifier.create(QName.create(SubscribeDeviceNotificationInput.QNAME, "path").intern());
- private static final NodeIdentifier DEVICE_NOTIFICATION_STREAM_PATH_NODEID =
- NodeIdentifier.create(QName.create(SubscribeDeviceNotificationInput.QNAME, "stream-path").intern());
-
- private static final NodeIdentifier SAL_REMOTE_OUTPUT_NODEID =
- NodeIdentifier.create(CreateDataChangeEventSubscriptionOutput.QNAME);
- private static final NodeIdentifier NOTIFICATIONS =
- NodeIdentifier.create(QName.create(CreateNotificationStreamInput.QNAME, "notifications").intern());
- private static final NodeIdentifier PATH_NODEID =
- NodeIdentifier.create(QName.create(CreateDataChangeEventSubscriptionInput.QNAME, "path").intern());
- private static final NodeIdentifier STREAM_NAME_NODEID =
- NodeIdentifier.create(QName.create(CreateDataChangeEventSubscriptionOutput.QNAME, "stream-name").intern());
-
- private final ConcurrentMap<String, RestconfStream<?>> streams = new ConcurrentHashMap<>();
- private final DOMDataBroker dataBroker;
- @Deprecated(forRemoval = true)
- private final DOMMountPointService mountPointService;
- @Deprecated(forRemoval = true)
- private final DOMNotificationService notificationService;
-
- private ListenersBroker(final DOMDataBroker dataBroker, final DOMNotificationService notificationService,
- final DOMMountPointService mountPointService) {
- this.dataBroker = requireNonNull(dataBroker);
- this.notificationService = requireNonNull(notificationService);
- this.mountPointService = requireNonNull(mountPointService);
- }
-
- /**
- * Get a {@link RestconfStream} by its name.
- *
- * @param streamName Stream name.
- * @return A {@link RestconfStream}, or {@code null} if the stream with specified name does not exist.
- * @throws NullPointerException if {@code streamName} is {@code null}
- */
- public final @Nullable RestconfStream<?> getStream(final String streamName) {
- return streams.get(streamName);
- }
-
- /**
- * Create a {@link RestconfStream} with a unique name. This method will atomically generate a stream name, create
- * the corresponding instance and register it.
- *
- * @param <T> Stream type
- * @param baseStreamLocation base streams location
- * @param factory Factory for creating the actual stream instance
- * @return A {@link RestconfStream} instance
- * @throws NullPointerException if {@code factory} is {@code null}
- */
- final <T> @NonNull RestconfFuture<RestconfStream<T>> createStream(final String description,
- final String baseStreamLocation, final Source<T> source) {
- final var stream = allocateStream(source);
- final var name = stream.name();
-
- // Now issue a put operation
- final var ret = new SettableRestconfFuture<RestconfStream<T>>();
- final var tx = dataBroker.newWriteOnlyTransaction();
- tx.put(LogicalDatastoreType.OPERATIONAL, restconfStateStreamPath(name),
- streamEntry(name, description, baseStreamLocation, stream.encodings()));
- tx.commit().addCallback(new FutureCallback<CommitInfo>() {
- @Override
- public void onSuccess(final CommitInfo result) {
- LOG.debug("Stream {} added", name);
- ret.set(stream);
- }
-
- @Override
- public void onFailure(final Throwable cause) {
- LOG.debug("Failed to add stream {}", name, cause);
- streams.remove(name, stream);
- ret.setFailure(new RestconfDocumentedException("Failed to allocate stream " + name, cause));
- }
- }, MoreExecutors.directExecutor());
- return ret;
- }
-
- private <T> @NonNull RestconfStream<T> allocateStream(final Source<T> source) {
- String name;
- RestconfStream<T> stream;
- do {
- // Use Type 4 (random) UUID. While we could just use it as a plain string, be nice to observers and anchor
- // it into UUID URN namespace as defined by RFC4122
- name = "urn:uuid:" + UUID.randomUUID().toString();
- stream = new RestconfStream<>(this, source, name);
- } while (streams.putIfAbsent(name, stream) != null);
-
- return stream;
- }
-
- /**
- * Remove a particular stream and remove its entry from operational datastore.
- *
- * @param stream Stream to remove
- */
- final void removeStream(final RestconfStream<?> stream) {
- // Defensive check to see if we are still tracking the stream
- final var streamName = stream.name();
- if (streams.get(streamName) != stream) {
- LOG.warn("Stream {} does not match expected instance {}, skipping datastore update", streamName, stream);
- return;
- }
-
- // Now issue a delete operation while the name is still protected by being associated in the map.
- final var tx = dataBroker.newWriteOnlyTransaction();
- tx.delete(LogicalDatastoreType.OPERATIONAL, restconfStateStreamPath(streamName));
- tx.commit().addCallback(new FutureCallback<CommitInfo>() {
- @Override
- public void onSuccess(final CommitInfo result) {
- LOG.debug("Stream {} removed", streamName);
- streams.remove(streamName, stream);
- }
-
- @Override
- public void onFailure(final Throwable cause) {
- LOG.warn("Failed to remove stream {}, operational datastore may be inconsistent", streamName, cause);
- streams.remove(streamName, stream);
- }
- }, MoreExecutors.directExecutor());
- }
-
- private static @NonNull YangInstanceIdentifier restconfStateStreamPath(final String streamName) {
- return RESTCONF_STATE_STREAMS.node(NodeIdentifierWithPredicates.of(Stream.QNAME, NAME_QNAME, streamName));
- }
-
- /**
- * Return the base location URL of the streams service based on request URI.
- *
- * @param baseURI request base URI
- * @throws IllegalArgumentException if the result would have been malformed
- */
- public final @NonNull String baseStreamLocation(final URI baseURI) {
- try {
- return new URI(streamsScheme(baseURI), baseURI.getRawUserInfo(), baseURI.getHost(), baseURI.getPort(),
- URLConstants.BASE_PATH + '/' + URLConstants.STREAMS_SUBPATH, null, null)
- .toString();
- } catch (URISyntaxException e) {
- throw new IllegalArgumentException("Cannot derive streams location", e);
- }
- }
-
- String streamsScheme(final URI baseURI) {
- return baseURI.getScheme();
- }
-
- /**
- * Create data-change-event stream with POST operation via RPC.
- *
- * @param input Input of RPC - example in JSON (data-change-event stream):
- * <pre>
- * {@code
- * {
- * "input": {
- * "path": "/toaster:toaster/toaster:toasterStatus",
- * "sal-remote-augment:datastore": "OPERATIONAL",
- * }
- * }
- * }
- * </pre>
- * @param modelContext Reference to {@link EffectiveModelContext}.
- * @return {@link DOMRpcResult} - Output of RPC - example in JSON:
- * <pre>
- * {@code
- * {
- * "output": {
- * "stream-name": "toaster:toaster/toaster:toasterStatus/datastore=OPERATIONAL/scope=ONE"
- * }
- * }
- * }
- * </pre>
- */
- // FIXME: this really should be a normal RPC implementation
- public final RestconfFuture<Optional<ContainerNode>> createDataChangeNotifiStream(
- final DatabindProvider databindProvider, final URI baseURI, final ContainerNode input,
- final EffectiveModelContext modelContext) {
- final var datastoreName = extractStringLeaf(input, DATASTORE_NODEID);
- final var datastore = datastoreName != null ? LogicalDatastoreType.valueOf(datastoreName)
- : LogicalDatastoreType.CONFIGURATION;
- final var path = preparePath(input);
-
- return createStream(
- "Events occuring in " + datastore + " datastore under /" + IdentifierCodec.serialize(path, modelContext),
- baseStreamLocation(baseURI), new DataTreeChangeSource(databindProvider, dataBroker, datastore, path))
- .transform(stream -> Optional.of(Builders.containerBuilder()
- .withNodeIdentifier(SAL_REMOTE_OUTPUT_NODEID)
- .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
- .build()));
- }
-
- // FIXME: this really should be a normal RPC implementation
- public final RestconfFuture<Optional<ContainerNode>> createNotificationStream(
- final DatabindProvider databindProvider, final URI baseURI, final ContainerNode input,
- final EffectiveModelContext modelContext) {
- final var qnames = ((LeafSetNode<String>) input.getChildByArg(NOTIFICATIONS)).body().stream()
- .map(LeafSetEntryNode::body)
- .map(QName::create)
- .sorted()
- .collect(ImmutableSet.toImmutableSet());
-
- final var description = new StringBuilder("YANG notifications matching any of {");
- var haveFirst = false;
- for (var qname : qnames) {
- final var module = modelContext.findModuleStatement(qname.getModule())
- .orElseThrow(() -> new RestconfDocumentedException(qname + " refers to an unknown module",
- ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
- final var stmt = module.findSchemaTreeNode(qname)
- .orElseThrow(() -> new RestconfDocumentedException(qname + " refers to an unknown notification",
- ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
- if (!(stmt instanceof NotificationEffectiveStatement)) {
- throw new RestconfDocumentedException(qname + " refers to a non-notification",
- ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
- }
-
- if (haveFirst) {
- description.append(",\n");
- } else {
- haveFirst = true;
- }
- description.append("\n ")
- .append(module.argument().getLocalName()).append(':').append(qname.getLocalName());
- }
- description.append("\n}");
-
- return createStream(description.toString(), baseStreamLocation(baseURI),
- new NotificationSource(databindProvider, notificationService, qnames))
- .transform(stream -> Optional.of(Builders.containerBuilder()
- .withNodeIdentifier(SAL_REMOTE_OUTPUT_NODEID)
- .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
- .build()));
- }
-
- /**
- * Create device notification stream.
- *
- * @param input RPC input
- * @return {@link DOMRpcResult} - Output of RPC - example in JSON
- */
- // FIXME: this should be an RPC invocation
- public final RestconfFuture<Optional<ContainerNode>> createDeviceNotificationStream(final URI baseURI,
- final ContainerNode input, final EffectiveModelContext modelContext) {
- // parsing out of container with settings and path
- // FIXME: ugly cast
- final var path = (YangInstanceIdentifier) input.findChildByArg(DEVICE_NOTIFICATION_PATH_NODEID)
- .map(DataContainerChild::body)
- .orElseThrow(() -> new RestconfDocumentedException("No path specified", ErrorType.APPLICATION,
- ErrorTag.DATA_MISSING));
-
- if (!(path.getLastPathArgument() instanceof NodeIdentifierWithPredicates listId)) {
- throw new RestconfDocumentedException("Path does not refer to a list item", ErrorType.APPLICATION,
- ErrorTag.INVALID_VALUE);
- }
- if (listId.size() != 1) {
- throw new RestconfDocumentedException("Target list uses multiple keys", ErrorType.APPLICATION,
- ErrorTag.INVALID_VALUE);
- }
-
- final var baseStreamsUri = baseStreamLocation(baseURI);
- return createStream(
- "All YANG notifications occuring on mount point /" + IdentifierCodec.serialize(path, modelContext),
- baseStreamsUri,
- new DeviceNotificationSource(mountPointService, path))
- .transform(stream -> Optional.of(Builders.containerBuilder()
- .withNodeIdentifier(new NodeIdentifier(SubscribeDeviceNotificationOutput.QNAME))
- .withChild(ImmutableNodes.leafNode(DEVICE_NOTIFICATION_STREAM_PATH_NODEID,
- baseStreamsUri + '/' + stream.name()))
- .build()));
- }
-
- /**
- * Prepare {@link YangInstanceIdentifier} of stream source.
- *
- * @param data Container with stream settings (RPC create-stream).
- * @return Parsed {@link YangInstanceIdentifier} of data element from which the data-change-event notifications
- * are going to be generated.
- */
- private static YangInstanceIdentifier preparePath(final ContainerNode data) {
- final var pathLeaf = data.childByArg(PATH_NODEID);
- if (pathLeaf != null && pathLeaf.body() instanceof YangInstanceIdentifier pathValue) {
- return pathValue;
- }
-
- throw new RestconfDocumentedException("Instance identifier was not normalized correctly",
- ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED);
- }
-
- private static @Nullable String extractStringLeaf(final ContainerNode data, final NodeIdentifier childName) {
- return data.childByArg(childName) instanceof LeafNode<?> leafNode && leafNode.body() instanceof String str
- ? str : null;
- }
-
- @VisibleForTesting
- static @NonNull MapEntryNode streamEntry(final String name, final String description,
- final String baseStreamLocation, final Set<EncodingName> encodings) {
- final var accessBuilder = Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(Access.QNAME));
- for (var encoding : encodings) {
- final var encodingName = encoding.name();
- accessBuilder.withChild(Builders.mapEntryBuilder()
- .withNodeIdentifier(NodeIdentifierWithPredicates.of(Access.QNAME, ENCODING_QNAME, encodingName))
- .withChild(ImmutableNodes.leafNode(ENCODING_QNAME, encodingName))
- .withChild(ImmutableNodes.leafNode(LOCATION_QNAME,
- baseStreamLocation + '/' + encodingName + '/' + name))
- .build());
- }
-
- return Builders.mapEntryBuilder()
- .withNodeIdentifier(NodeIdentifierWithPredicates.of(Stream.QNAME, NAME_QNAME, name))
- .withChild(ImmutableNodes.leafNode(NAME_QNAME, name))
- .withChild(ImmutableNodes.leafNode(DESCRIPTION_QNAME, description))
- .withChild(accessBuilder.build())
- .build();
- }
-}
--- /dev/null
+/*
+ * 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.streams;
+
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.concepts.Registration;
+
+@NonNullByDefault
+public interface PingExecutor {
+
+ Registration startPingProcess(Runnable task, long delay, TimeUnit timeUnit);
+}
--- /dev/null
+/*
+ * 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.streams;
+
+import javax.servlet.http.HttpServlet;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+
+/**
+ * A helper for creating {@link HttpServlet}s which provide bridge between JAX-RS and {@link RestconfStream.Registry}.
+ */
+public interface RestconfStreamServletFactory {
+
+ @NonNull HttpServlet newStreamServlet();
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 Lumina Networks, Inc. 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.streams;
+
+import java.util.Set;
+import javax.ws.rs.core.Application;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+
+/**
+ * JAX-RS binding for Server-Sent Events.
+ */
+final class SSEApplication extends Application {
+ private final SSEStreamService singleton;
+
+ SSEApplication(final RestconfStream.Registry streamRegistry, final PingExecutor pingExecutor,
+ final StreamsConfiguration configuration) {
+ singleton = new SSEStreamService(streamRegistry, pingExecutor, configuration);
+ }
+
+ @Override
+ public Set<Object> getSingletons() {
+ return Set.of(singleton);
+ }
+}
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import java.io.UnsupportedEncodingException;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import javax.xml.xpath.XPathExpressionException;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sender;
import org.opendaylight.yangtools.concepts.Registration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* SSE session handler that is responsible for controlling of session, managing subscription to data-change-event or
* notification listener, and sending of data over established SSE session.
*/
-public final class SSESessionHandler implements StreamSessionHandler {
- private static final Logger LOG = LoggerFactory.getLogger(SSESessionHandler.class);
+final class SSESender implements Sender {
+ private static final Logger LOG = LoggerFactory.getLogger(SSESender.class);
private static final CharMatcher CR_OR_LF = CharMatcher.anyOf("\r\n");
- private final ScheduledExecutorService executorService;
+ private final PingExecutor pingExecutor;
private final RestconfStream<?> stream;
private final EncodingName encoding;
private final ReceiveEventsParams params;
private final SseEventSink sink;
private final Sse sse;
private final int maximumFragmentLength;
- private final int heartbeatInterval;
+ private final long heartbeatMillis;
- private ScheduledFuture<?> pingProcess;
+ private Registration pingProcess;
private Registration subscriber;
/**
* Creation of the new server-sent events session handler.
*
- * @param executorService Executor that is used for periodical sending of SSE ping messages to keep session up even
+ * @param pingExecutor Executor that is used for periodical sending of SSE ping messages to keep session up even
* if the notifications doesn't flow from server to clients or clients don't implement ping-pong
* service.
* @param stream YANG notification or data-change event listener to which client on this SSE session subscribes to.
* (exceeded notification length ends in error). If the parameter is set to non-zero positive value,
* messages longer than this parameter are fragmented into multiple SSE messages sent in one
* transaction.
- * @param heartbeatInterval Interval in milliseconds of sending of ping control frames to remote endpoint to keep
+ * @param heartbeatMillis Interval in milliseconds of sending of ping control frames to remote endpoint to keep
* session up. Ping control frames are disabled if this parameter is set to 0.
*/
- public SSESessionHandler(final ScheduledExecutorService executorService, final SseEventSink sink, final Sse sse,
- final RestconfStream<?> stream, final EncodingName encoding, final ReceiveEventsParams params,
- final int maximumFragmentLength, final int heartbeatInterval) {
- this.executorService = requireNonNull(executorService);
+ SSESender(final PingExecutor pingExecutor, final SseEventSink sink, final Sse sse, final RestconfStream<?> stream,
+ final EncodingName encoding, final ReceiveEventsParams params, final int maximumFragmentLength,
+ final long heartbeatMillis) {
+ this.pingExecutor = requireNonNull(pingExecutor);
this.sse = requireNonNull(sse);
this.sink = requireNonNull(sink);
this.stream = requireNonNull(stream);
this.encoding = requireNonNull(encoding);
this.params = requireNonNull(params);
this.maximumFragmentLength = maximumFragmentLength;
- this.heartbeatInterval = heartbeatInterval;
+ this.heartbeatMillis = heartbeatMillis;
}
/**
}
subscriber = local;
- if (heartbeatInterval != 0) {
- pingProcess = executorService.scheduleWithFixedDelay(this::sendPingMessage, heartbeatInterval,
- heartbeatInterval, TimeUnit.MILLISECONDS);
+ if (heartbeatMillis != 0) {
+ pingProcess = pingExecutor.startPingProcess(this::sendPing, heartbeatMillis, TimeUnit.MILLISECONDS);
}
return true;
}
return outputMessage.toString();
}
- private synchronized void sendPingMessage() {
+ private synchronized void sendPing() {
if (!sink.isClosed()) {
LOG.debug("sending PING");
sink.send(sse.newEventBuilder().comment("ping").build());
}
private void stopPingProcess() {
- if (pingProcess != null && !pingProcess.isDone() && !pingProcess.isCancelled()) {
- pingProcess.cancel(true);
+ if (pingProcess != null) {
+ pingProcess.close();
+ pingProcess = null;
}
}
* 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.rfc8040.streams;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableMap;
import java.io.UnsupportedEncodingException;
-import java.util.concurrent.ScheduledExecutorService;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import javax.xml.xpath.XPathExpressionException;
-import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
-import org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
-import org.opendaylight.restconf.nb.rfc8040.streams.SSESessionHandler;
-import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* Access to notification streams via Server-Sent Events.
*/
@Path("/")
-public final class RestconfDataStreamServiceImpl {
- private static final Logger LOG = LoggerFactory.getLogger(RestconfDataStreamServiceImpl.class);
+final class SSEStreamService {
+ private static final Logger LOG = LoggerFactory.getLogger(SSEStreamService.class);
- private final ListenersBroker listenersBroker;
- private final ScheduledExecutorService executorService;
+ private final RestconfStream.Registry streamRegistry;
+ private final PingExecutor pingExecutor;
private final int maximumFragmentLength;
private final int heartbeatInterval;
- public RestconfDataStreamServiceImpl(final ScheduledThreadPool scheduledThreadPool,
- final ListenersBroker listenersBroker, final StreamsConfiguration configuration) {
- executorService = scheduledThreadPool.getExecutor();
- this.listenersBroker = requireNonNull(listenersBroker);
+ SSEStreamService(final RestconfStream.Registry streamRegistry, final PingExecutor pingExecutor,
+ final StreamsConfiguration configuration) {
+ this.streamRegistry = requireNonNull(streamRegistry);
+ this.pingExecutor = requireNonNull(pingExecutor);
heartbeatInterval = configuration.heartbeatInterval();
maximumFragmentLength = configuration.maximumFragmentLength();
}
public void getSSE(@PathParam("encodingName") final EncodingName encodingName,
@PathParam("streamName") final String streamName, @Context final UriInfo uriInfo,
@Context final SseEventSink sink, @Context final Sse sse) {
- final var stream = listenersBroker.getStream(streamName);
+ final var stream = streamRegistry.lookupStream(streamName);
if (stream == null) {
LOG.debug("Listener for stream with name {} was not found.", streamName);
throw new NotFoundException("No such stream: " + streamName);
LOG.debug("Listener for stream with name {} has been found, SSE session handler will be created.", streamName);
// FIXME: invert control here: we should call 'listener.addSession()', which in turn should call
// handler.init()/handler.close()
- final var handler = new SSESessionHandler(executorService, sink, sse, stream, encodingName, params,
+ final var handler = new SSESender(pingExecutor, sink, sse, stream, encodingName, params,
maximumFragmentLength, heartbeatInterval);
try {
+++ /dev/null
-/*
- * Copyright (c) 2020 Lumina Networks, Inc. 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.streams;
-
-/**
- * Interface for session handler that is responsible for sending of data over established session.
- */
-public interface StreamSessionHandler {
- /**
- * Interface for sending String message through one of implementation.
- *
- * @param data Message data to be send.
- */
- void sendDataMessage(String data);
-
- /**
- * Called when the stream has reached its end. The handler should close all underlying resources.
- */
- void endOfStream();
-}
* (exceeded message length leads to fragmentation of messages).
* @param idleTimeout Maximum idle time of web-socket session before the session is closed (milliseconds).
* @param heartbeatInterval Interval in milliseconds between sending of ping control frames.
- * @param useSSE when is {@code true} use SSE else use WS
*/
-public record StreamsConfiguration(int maximumFragmentLength, int idleTimeout, int heartbeatInterval, boolean useSSE) {
+public record StreamsConfiguration(int maximumFragmentLength, int idleTimeout, int heartbeatInterval) {
// FIXME: can this be 64KiB exactly? if so, maximumFragmentLength should become a Uint16 and validation should be
// pushed out to users
public static final int MAXIMUM_FRAGMENT_LENGTH_LIMIT = 65534;
import static java.util.Objects.requireNonNull;
-import java.util.concurrent.ScheduledExecutorService;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.opendaylight.restconf.nb.rfc8040.URLConstants;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* @param heartbeatInterval Interval in milliseconds between sending of ping control frames.
*/
record WebSocketFactory(
- ScheduledExecutorService executorService,
- ListenersBroker listenersBroker,
+ RestconfStream.Registry streamRegistry,
+ PingExecutor pingExecutor,
int maximumFragmentLength,
int heartbeatInterval) implements WebSocketCreator {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketFactory.class);
"/" + URLConstants.BASE_PATH + "/" + URLConstants.STREAMS_SUBPATH + "/";
WebSocketFactory {
- requireNonNull(executorService);
- requireNonNull(listenersBroker);
+ requireNonNull(pingExecutor);
+ requireNonNull(streamRegistry);
}
/**
return notFound(resp);
}
final var streamName = stripped.substring(slash + 1);
- final var stream = listenersBroker.getStream(streamName);
+ final var stream = streamRegistry.lookupStream(streamName);
if (stream == null) {
LOG.debug("Listener for stream with name {} was not found.", streamName);
return notFound(resp);
resp.setStatusCode(HttpServletResponse.SC_SWITCHING_PROTOCOLS);
// note: every web-socket manages PING process individually because this approach scales better than
// sending PING frames at once over all web-socket sessions
- return new WebSocketSessionHandler(executorService, stream, new EncodingName(stripped.substring(0, slash)),
+ return new WebSocketSender(pingExecutor, stream, new EncodingName(stripped.substring(0, slash)),
null, maximumFragmentLength, heartbeatInterval);
}
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
-import javax.inject.Inject;
-import javax.inject.Singleton;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
-import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
+import org.opendaylight.restconf.server.spi.RestconfStream;
/**
* Web-socket servlet listening on ws or wss schemas for created data-change-event or notification streams.
*/
-@Singleton
-public final class WebSocketInitializer extends WebSocketServlet {
+final class WebSocketInitializer extends WebSocketServlet {
@java.io.Serial
private static final long serialVersionUID = 1L;
private final transient WebSocketFactory creator;
private final int idleTimeoutMillis;
- /**
- * Creation of the web-socket initializer.
- *
- * @param scheduledThreadPool ODL thread pool used for fetching of scheduled executors.
- * @param configuration Web-socket configuration holder.
- */
- @Inject
- public WebSocketInitializer(final ScheduledThreadPool scheduledThreadPool,
- final ListenersBroker listenersBroker, final StreamsConfiguration configuration) {
- creator = new WebSocketFactory(scheduledThreadPool.getExecutor(), listenersBroker,
+ WebSocketInitializer(final RestconfStream.Registry streamRegistry, final PingExecutor pingExecutor,
+ final StreamsConfiguration configuration) {
+ creator = new WebSocketFactory(streamRegistry, pingExecutor,
configuration.maximumFragmentLength(), configuration.heartbeatInterval());
idleTimeoutMillis = configuration.idleTimeout();
}
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.xpath.XPathExpressionException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sender;
import org.opendaylight.yangtools.concepts.Registration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* to data-change-event or notification listener, and sending of data over established web-socket session.
*/
@WebSocket
-public final class WebSocketSessionHandler implements StreamSessionHandler {
- private static final Logger LOG = LoggerFactory.getLogger(WebSocketSessionHandler.class);
+final class WebSocketSender implements Sender {
+ private static final Logger LOG = LoggerFactory.getLogger(WebSocketSender.class);
private static final byte[] PING_PAYLOAD = "ping".getBytes(Charset.defaultCharset());
- private final ScheduledExecutorService executorService;
+ private final PingExecutor pingExecutor;
private final RestconfStream<?> stream;
private final EncodingName encodingName;
private final ReceiveEventsParams params;
private final int maximumFragmentLength;
- private final int heartbeatInterval;
+ private final long heartbeatInterval;
private Session session;
private Registration subscriber;
- private ScheduledFuture<?> pingProcess;
+ private Registration pingProcess;
/**
* Creation of the new web-socket session handler.
*
- * @param executorService Executor that is used for periodical sending of web-socket ping messages to keep
+ * @param pingExecutor Executor that is used for periodical sending of web-socket ping messages to keep
* session up even if the notifications doesn't flow from server to clients or clients
* don't implement ping-pong service.
* @param stream YANG notification or data-change event listener to which client on this web-socket
* @param heartbeatInterval Interval in milliseconds of sending of ping control frames to remote endpoint
* to keep session up. Ping control frames are disabled if this parameter is set to 0.
*/
- WebSocketSessionHandler(final ScheduledExecutorService executorService, final RestconfStream<?> stream,
- final EncodingName encodingName, final @Nullable ReceiveEventsParams params,
- final int maximumFragmentLength, final int heartbeatInterval) {
- this.executorService = requireNonNull(executorService);
+ WebSocketSender(final PingExecutor pingExecutor, final RestconfStream<?> stream, final EncodingName encodingName,
+ final @Nullable ReceiveEventsParams params, final int maximumFragmentLength, final long heartbeatInterval) {
+ this.pingExecutor = requireNonNull(pingExecutor);
this.stream = requireNonNull(stream);
this.encodingName = requireNonNull(encodingName);
// FIXME: NETCONF-1102: require params
if (heartbeatInterval != 0) {
// sending of PING frame can be long if there is an error on web-socket - from this reason
// the fixed-rate should not be used
- pingProcess = executorService.scheduleWithFixedDelay(this::sendPingMessage, heartbeatInterval,
- heartbeatInterval, TimeUnit.MILLISECONDS);
+ pingProcess = pingExecutor.startPingProcess(this::sendPing, heartbeatInterval, TimeUnit.MILLISECONDS);
}
}
}
}
private void stopPingProcess() {
- if (pingProcess != null && !pingProcess.isDone() && !pingProcess.isCancelled()) {
- pingProcess.cancel(true);
+ if (pingProcess != null) {
+ pingProcess.close();
+ pingProcess = null;
}
}
}
}
- private synchronized void sendPingMessage() {
+ private synchronized void sendPing() {
try {
Objects.requireNonNull(session).getRemote().sendPing(ByteBuffer.wrap(PING_PAYLOAD));
} catch (IOException e) {
--- /dev/null
+/*
+ * 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 java.net.URI;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
+import org.opendaylight.restconf.server.spi.OperationOutput;
+
+/**
+ * An implementation of a RESTCONF server, implementing the
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.3">RESTCONF API Resource</a>.
+ */
+@NonNullByDefault
+public interface RestconfServer {
+
+ // FIXME: use ApiPath instead of String
+ RestconfFuture<OperationOutput> invokeRpc(URI restconfURI, String apiPath, OperationInputBody body);
+}
--- /dev/null
+/*
+ * 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
+ */
+/**
+ * Interface to a RESTCONF server instance. The primary entry point is {@link RestconfServer}.
+ */
+package org.opendaylight.restconf.server.api;
\ No newline at end of file
* 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;
+package org.opendaylight.restconf.server.mdsal;
import static java.util.Objects.requireNonNull;
import static org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.$YangModuleInfoImpl.qnameOf;
--- /dev/null
+/*
+ * Copyright © 2019 FRINX s.r.o. 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.util.concurrent.ListenableFuture;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.restconf.server.spi.AbstractRestconfStreamRegistry;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.RestconfState;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.Streams;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.Stream;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * This singleton class is responsible for creation, removal and searching for {@link RestconfStream}s.
+ */
+@Singleton
+@Component(factory = MdsalRestconfStreamRegistry.FACTORY_NAME, service = RestconfStream.Registry.class)
+public final class MdsalRestconfStreamRegistry extends AbstractRestconfStreamRegistry {
+ public static final String FACTORY_NAME = "org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker";
+
+ private static final YangInstanceIdentifier RESTCONF_STATE_STREAMS = YangInstanceIdentifier.of(
+ NodeIdentifier.create(RestconfState.QNAME),
+ NodeIdentifier.create(Streams.QNAME),
+ NodeIdentifier.create(Stream.QNAME));
+ private static final String USE_WEBSOCKETS_PROP = ".useWebsockets";
+
+ private final DOMDataBroker dataBroker;
+
+ public MdsalRestconfStreamRegistry(final DOMDataBroker dataBroker, final boolean useWebsockets) {
+ super(useWebsockets);
+ this.dataBroker = requireNonNull(dataBroker);
+ }
+
+ @Inject
+ public MdsalRestconfStreamRegistry(final DOMDataBroker dataBroker) {
+ this(dataBroker, false);
+ }
+
+ @Activate
+ public MdsalRestconfStreamRegistry(@Reference final DOMDataBroker dataBroker, final Map<String, ?> props) {
+ this(dataBroker, (boolean) requireNonNull(props.get(USE_WEBSOCKETS_PROP)));
+ }
+
+ public static Map<String, ?> props(final boolean useSSE) {
+ return Map.of(USE_WEBSOCKETS_PROP, !useSSE);
+ }
+
+ @Override
+ protected ListenableFuture<?> putStream(final MapEntryNode stream) {
+ // Now issue a put operation
+ final var tx = dataBroker.newWriteOnlyTransaction();
+ tx.put(LogicalDatastoreType.OPERATIONAL, RESTCONF_STATE_STREAMS.node(stream.name()), stream);
+ return tx.commit();
+ }
+
+ @Override
+ protected ListenableFuture<?> deleteStream(final NodeIdentifierWithPredicates streamName) {
+ // Now issue a delete operation while the name is still protected by being associated in the map.
+ final var tx = dataBroker.newWriteOnlyTransaction();
+ tx.delete(LogicalDatastoreType.OPERATIONAL, RESTCONF_STATE_STREAMS.node(streamName));
+ return tx.commit();
+ }
+}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.devnotif;
import static java.util.Objects.requireNonNull;
import org.opendaylight.mdsal.dom.api.DOMNotification;
import org.opendaylight.mdsal.dom.api.DOMNotificationService;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Sink;
+import org.opendaylight.restconf.server.mdsal.streams.notif.AbstractNotificationSource;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sink;
import org.opendaylight.yangtools.concepts.Registration;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
/**
* A {@link RestconfStream} reporting YANG notifications coming from a mounted device.
*/
-public final class DeviceNotificationSource extends AbstractNotificationSource implements DOMMountPointListener {
+final class DeviceNotificationSource extends AbstractNotificationSource implements DOMMountPointListener {
private static final Logger LOG = LoggerFactory.getLogger(DeviceNotificationSource.class);
private final AtomicReference<Runnable> onRemoved = new AtomicReference<>();
return endOfStream(sink);
}
- final var notifReg = optNotification.orElseThrow().registerNotificationListener(
- new Listener(sink, () -> modelContext), paths);
+ final var notifReg = optNotification.orElseThrow()
+ .registerNotificationListener(new Listener(sink, () -> modelContext), paths);
// Notifications are running now.
// If we get removed we need to close those. But since we are running lockless and we need to set up
--- /dev/null
+/*
+ * 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.streams.devnotif;
+
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.opendaylight.mdsal.dom.api.DOMMountPointService;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
+import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.device.notification.rev221106.SubscribeDeviceNotification;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.device.notification.rev221106.SubscribeDeviceNotificationInput;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.device.notification.rev221106.SubscribeDeviceNotificationOutput;
+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.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * RESTCONF implementation of {@link SubscribeDeviceNotification}.
+ */
+@Singleton
+@Component
+public final class SubscribeDeviceNotificationRpc extends RpcImplementation {
+ private static final NodeIdentifier DEVICE_NOTIFICATION_PATH_NODEID =
+ NodeIdentifier.create(QName.create(SubscribeDeviceNotificationInput.QNAME, "path").intern());
+ // FIXME: NETCONF-1102: this should be 'stream-name'
+ private static final NodeIdentifier DEVICE_NOTIFICATION_STREAM_PATH_NODEID =
+ NodeIdentifier.create(QName.create(SubscribeDeviceNotificationInput.QNAME, "stream-path").intern());
+
+ private final DOMMountPointService mountPointService;
+ private final RestconfStream.Registry streamRegistry;
+
+ @Inject
+ @Activate
+ public SubscribeDeviceNotificationRpc(@Reference final RestconfStream.Registry streamRegistry,
+ @Reference final DOMMountPointService mountPointService) {
+ super(SubscribeDeviceNotification.QNAME);
+ this.mountPointService = requireNonNull(mountPointService);
+ this.streamRegistry = requireNonNull(streamRegistry);
+ }
+
+ @Override
+ public RestconfFuture<OperationOutput> invoke(final URI restconfURI, final OperationInput input) {
+ final var body = input.input();
+ final var pathLeaf = body.childByArg(DEVICE_NOTIFICATION_PATH_NODEID);
+ if (pathLeaf == null) {
+ return RestconfFuture.failed(new RestconfDocumentedException("No path specified", ErrorType.APPLICATION,
+ ErrorTag.MISSING_ELEMENT));
+ }
+ final var pathLeafBody = pathLeaf.body();
+ if (!(pathLeafBody instanceof YangInstanceIdentifier path)) {
+ return RestconfFuture.failed(new RestconfDocumentedException("Unexpected path " + pathLeafBody,
+ ErrorType.APPLICATION, ErrorTag.BAD_ELEMENT));
+ }
+ if (!(path.getLastPathArgument() instanceof NodeIdentifierWithPredicates listId)) {
+ return RestconfFuture.failed(new RestconfDocumentedException(path + " does not refer to a list item",
+ ErrorType.APPLICATION, ErrorTag.BAD_ELEMENT));
+ }
+ if (listId.size() != 1) {
+ return RestconfFuture.failed(new RestconfDocumentedException(path + " uses multiple keys",
+ ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
+ }
+
+ return streamRegistry.createStream(restconfURI, new DeviceNotificationSource(mountPointService, path),
+ "All YANG notifications occuring on mount point /"
+ + IdentifierCodec.serialize(path, input.currentContext().modelContext()))
+ .transform(stream -> input.newOperationOutput(Builders.containerBuilder()
+ .withNodeIdentifier(new NodeIdentifier(SubscribeDeviceNotificationOutput.QNAME))
+ .withChild(ImmutableNodes.leafNode(DEVICE_NOTIFICATION_STREAM_PATH_NODEID, stream.name()))
+ .build()));
+ }
+}
--- /dev/null
+/*
+ * 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
+ */
+/**
+ * Support for streams of YANG 1.0 notifications coming from a mounted device.
+ */
+package org.opendaylight.restconf.server.mdsal.streams.devnotif;
\ No newline at end of file
--- /dev/null
+/*
+ * 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.streams.dtcl;
+
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
+import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscription;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscriptionInput;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscriptionOutput;
+import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev231103.CreateDataChangeEventSubscriptionInput1;
+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.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * RESTCONF implementation of {@link CreateDataChangeEventSubscription}.
+ */
+@Singleton
+@Component
+public final class CreateDataChangeEventSubscriptionRpc extends RpcImplementation {
+ private static final @NonNull NodeIdentifier DATASTORE_NODEID = NodeIdentifier.create(
+ QName.create(CreateDataChangeEventSubscriptionInput1.QNAME, "datastore").intern());
+ private static final @NonNull NodeIdentifier STREAM_NAME_NODEID =
+ NodeIdentifier.create(QName.create(CreateDataChangeEventSubscriptionOutput.QNAME, "stream-name").intern());
+ private static final @NonNull NodeIdentifier PATH_NODEID =
+ NodeIdentifier.create(QName.create(CreateDataChangeEventSubscriptionInput.QNAME, "path").intern());
+ private static final @NonNull NodeIdentifier OUTPUT_NODEID =
+ NodeIdentifier.create(CreateDataChangeEventSubscriptionOutput.QNAME);
+
+ private final DatabindProvider databindProvider;
+ private final DOMDataTreeChangeService changeService;
+ private final RestconfStream.Registry streamRegistry;
+
+ @Inject
+ @Activate
+ public CreateDataChangeEventSubscriptionRpc(@Reference final RestconfStream.Registry streamRegistry,
+ @Reference final DatabindProvider databindProvider, @Reference final DOMDataBroker dataBroker) {
+ super(CreateDataChangeEventSubscription.QNAME);
+ this.databindProvider = requireNonNull(databindProvider);
+ changeService = dataBroker.getExtensions().getInstance(DOMDataTreeChangeService.class);
+ if (changeService == null) {
+ throw new UnsupportedOperationException("DOMDataBroker does not support the DOMDataTreeChangeService");
+ }
+ this.streamRegistry = requireNonNull(streamRegistry);
+ }
+
+ /**
+ * Create data-change-event stream with POST operation via RPC.
+ *
+ * @param input Input of RPC - example in JSON (data-change-event stream):
+ * <pre>
+ * {@code
+ * {
+ * "input": {
+ * "path": "/toaster:toaster/toaster:toasterStatus",
+ * "sal-remote-augment:datastore": "OPERATIONAL",
+ * }
+ * }
+ * }
+ * </pre>
+ * @return Future output of RPC - example in JSON:
+ * <pre>
+ * {@code
+ * {
+ * "output": {
+ * "stream-name": "toaster:toaster/toaster:toasterStatus/datastore=OPERATIONAL/scope=ONE"
+ * }
+ * }
+ * }
+ * </pre>
+ */
+ @Override
+ public RestconfFuture<OperationOutput> invoke(final URI restconfURI, final OperationInput input) {
+ final var body = input.input();
+ final var datastoreName = leaf(body, DATASTORE_NODEID, String.class);
+ final var datastore = datastoreName != null ? LogicalDatastoreType.valueOf(datastoreName)
+ : LogicalDatastoreType.CONFIGURATION;
+
+ final var path = leaf(body, PATH_NODEID, YangInstanceIdentifier.class);
+ if (path == null) {
+ return RestconfFuture.failed(
+ new RestconfDocumentedException("missing path", ErrorType.APPLICATION, ErrorTag.MISSING_ELEMENT));
+ }
+
+ return streamRegistry.createStream(restconfURI,
+ new DataTreeChangeSource(databindProvider, changeService, datastore, path),
+ "Events occuring in " + datastore + " datastore under /"
+ + IdentifierCodec.serialize(path, input.currentContext().modelContext()))
+ .transform(stream -> input.newOperationOutput(Builders.containerBuilder()
+ .withNodeIdentifier(OUTPUT_NODEID)
+ .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
+ .build()));
+ }
+}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import java.io.IOException;
import java.time.Instant;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.dom.DOMResult;
import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.restconf.server.spi.EventFormatter;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.DataChangedNotification;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.DataChangeEvent;
import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
}
@Override
- final void fillDocument(final Document doc, final EffectiveModelContext schemaContext,
+ protected final void fillDocument(final Document doc, final EffectiveModelContext schemaContext,
final List<DataTreeCandidate> input) throws IOException {
final var notificationElement = createNotificationElement(doc, Instant.now());
final var notificationEventElement = doc.createElementNS(DATA_CHANGED_NOTIFICATION_NS,
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import java.util.List;
+import org.opendaylight.restconf.server.spi.EventFormatterFactory;
import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
abstract class DataTreeCandidateFormatterFactory extends EventFormatterFactory<List<DataTreeCandidate>> {
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
import java.util.Deque;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.DataChangeEvent;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.DataChangeEvent.Operation;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.data.change.event.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-abstract class AbstractWebsocketSerializer<T extends Exception> {
- private static final Logger LOG = LoggerFactory.getLogger(AbstractWebsocketSerializer.class);
+abstract class DataTreeCandidateSerializer<T extends Exception> {
+ private static final Logger LOG = LoggerFactory.getLogger(DataTreeCandidateSerializer.class);
static final @NonNull QName PATH_QNAME = QName.create(DataChangeEvent.QNAME, "path").intern();
static final @NonNull NodeIdentifier PATH_NID = NodeIdentifier.create(PATH_QNAME);
static final @NonNull QName OPERATION_QNAME = QName.create(DataChangeEvent.QNAME, "operation").intern();
private final EffectiveModelContext context;
- AbstractWebsocketSerializer(final EffectiveModelContext context) {
+ DataTreeCandidateSerializer(final EffectiveModelContext context) {
this.context = requireNonNull(context);
}
- public final boolean serialize(final DataTreeCandidate candidate, final TextParameters params) throws T {
+ final boolean serialize(final DataTreeCandidate candidate, final TextParameters params) throws T {
final var skipData = params.skipData();
final var changedLeafNodesOnly = params.changedLeafNodesOnly();
if (changedLeafNodesOnly || params.leafNodesOnly()) {
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import static java.util.Objects.requireNonNull;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
-import org.opendaylight.mdsal.dom.api.DOMDataBroker;
import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService;
import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Sink;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Source;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sink;
+import org.opendaylight.restconf.server.spi.RestconfStream.Source;
import org.opendaylight.yangtools.concepts.Registration;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
/**
* A {@link RestconfStream} reporting changes on a particular data tree.
*/
+@VisibleForTesting
public final class DataTreeChangeSource extends Source<List<DataTreeCandidate>> {
private static final ImmutableMap<EncodingName, DataTreeCandidateFormatterFactory> ENCODINGS = ImmutableMap.of(
EncodingName.RFC8040_JSON, JSONDataTreeCandidateFormatter.FACTORY,
private final @NonNull LogicalDatastoreType datastore;
private final @NonNull YangInstanceIdentifier path;
- DataTreeChangeSource(final DatabindProvider databindProvider, final DOMDataBroker dataBroker,
+ public DataTreeChangeSource(final DatabindProvider databindProvider, final DOMDataTreeChangeService changeService,
final LogicalDatastoreType datastore, final YangInstanceIdentifier path) {
super(ENCODINGS);
this.databindProvider = requireNonNull(databindProvider);
+ this.changeService = requireNonNull(changeService);
this.datastore = requireNonNull(datastore);
this.path = requireNonNull(path);
-
- final var dtcs = dataBroker.getExtensions().getInstance(DOMDataTreeChangeService.class);
- if (dtcs == null) {
- throw new UnsupportedOperationException("DOMDataBroker does not support the DOMDataTreeChangeService");
- }
- changeService = dtcs;
}
@Override
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.List;
import javax.xml.xpath.XPathExpressionException;
import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.$YangModuleInfoImpl;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.DataChangedNotification;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.DataChangeEvent;
import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-public final class JSONDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
+final class JSONDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
private static final @NonNull String DATA_CHANGED_EVENT_NAME = DataChangeEvent.QNAME.getLocalName();
private static final @NonNull String DATA_CHANGED_NOTIFICATION_NAME =
$YangModuleInfoImpl.getInstance().getName().getLocalName() + ":" + DataChangedNotification.QNAME.getLocalName();
}
@Override
- String createText(final TextParameters params, final EffectiveModelContext schemaContext,
+ protected String createText(final TextParameters params, final EffectiveModelContext schemaContext,
final List<DataTreeCandidate> input, final Instant now) throws IOException {
try (var writer = new StringWriter()) {
boolean nonEmpty = false;
.name(DATA_CHANGED_NOTIFICATION_NAME).beginObject()
.name(DATA_CHANGED_EVENT_NAME).beginArray();
- final var serializer = new JsonDataTreeCandidateSerializer(schemaContext, jsonWriter);
+ final var serializer = new JSONDataTreeCandidateSerializer(schemaContext, jsonWriter);
for (var candidate : input) {
nonEmpty |= serializer.serialize(candidate, params);
}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import static java.util.Objects.requireNonNull;
import static org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter.createNestedWriter;
import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
-final class JsonDataTreeCandidateSerializer extends AbstractWebsocketSerializer<IOException> {
+final class JSONDataTreeCandidateSerializer extends DataTreeCandidateSerializer<IOException> {
private static final XMLNamespace SAL_REMOTE_NS = DataChangedNotification.QNAME.getNamespace();
private static final Absolute DATA_CHANGE_EVENT = Absolute.of(DataChangedNotification.QNAME, DataChangeEvent.QNAME);
private final JsonWriter jsonWriter;
- JsonDataTreeCandidateSerializer(final EffectiveModelContext context, final JsonWriter jsonWriter) {
+ JSONDataTreeCandidateSerializer(final EffectiveModelContext context, final JsonWriter jsonWriter) {
super(context);
this.jsonWriter = requireNonNull(jsonWriter);
}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import java.io.IOException;
import java.io.StringWriter;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-public final class XMLDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
+final class XMLDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
private static final XMLDataTreeCandidateFormatter EMPTY = new XMLDataTreeCandidateFormatter(TextParameters.EMPTY);
static final DataTreeCandidateFormatterFactory FACTORY = new DataTreeCandidateFormatterFactory(EMPTY) {
}
@Override
- String createText(final TextParameters params, final EffectiveModelContext schemaContext,
+ protected String createText(final TextParameters params, final EffectiveModelContext schemaContext,
final List<DataTreeCandidate> input, final Instant now) throws Exception {
final var writer = new StringWriter();
boolean nonEmpty = false;
try {
- final var xmlStreamWriter = NotificationFormatter.createStreamWriterWithNotification(writer, now);
+ final var xmlStreamWriter = createStreamWriterWithNotification(writer, now);
xmlStreamWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, DATA_CHANGED_NOTIFICATION_ELEMENT,
DATA_CHANGED_NOTIFICATION_NS);
xmlStreamWriter.writeDefaultNamespace(DATA_CHANGED_NOTIFICATION_NS);
- final var serializer = new XmlDataTreeCandidateSerializer(schemaContext, xmlStreamWriter);
+ final var serializer = new XMLDataTreeCandidateSerializer(schemaContext, xmlStreamWriter);
for (var candidate : input) {
nonEmpty |= serializer.serialize(candidate, params);
}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import static java.util.Objects.requireNonNull;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
-final class XmlDataTreeCandidateSerializer extends AbstractWebsocketSerializer<Exception> {
+final class XMLDataTreeCandidateSerializer extends DataTreeCandidateSerializer<Exception> {
private static final @NonNull NodeIdentifier DATA_CHANGE_EVENT_NID = NodeIdentifier.create(DataChangeEvent.QNAME);
private final XMLStreamWriter xmlWriter;
- XmlDataTreeCandidateSerializer(final EffectiveModelContext context, final XMLStreamWriter xmlWriter) {
+ XMLDataTreeCandidateSerializer(final EffectiveModelContext context, final XMLStreamWriter xmlWriter) {
super(context);
this.xmlWriter = requireNonNull(xmlWriter);
}
--- /dev/null
+/*
+ * 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
+ */
+/**
+ * Support for data tree change streams.
+ */
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
\ No newline at end of file
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import static java.util.Objects.requireNonNull;
import org.opendaylight.mdsal.dom.api.DOMEvent;
import org.opendaylight.mdsal.dom.api.DOMNotification;
import org.opendaylight.mdsal.dom.api.DOMNotificationListener;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Sink;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Source;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sink;
+import org.opendaylight.restconf.server.spi.RestconfStream.Source;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextProvider;
/**
- * Abstract base class for functionality shared between {@link NotificationSource} and
- * {@link DeviceNotificationSource}.
+ * Abstract base class for functionality shared between {@link DOMNotification}-based sources.
*/
-abstract class AbstractNotificationSource extends Source<DOMNotification> {
- static final class Listener implements DOMNotificationListener {
+public abstract class AbstractNotificationSource extends Source<DOMNotification> {
+ protected static final class Listener implements DOMNotificationListener {
private final Sink<DOMNotification> sink;
private final EffectiveModelContextProvider modelContext;
- Listener(final Sink<DOMNotification> sink, final EffectiveModelContextProvider modelContext) {
+ public Listener(final Sink<DOMNotification> sink, final EffectiveModelContextProvider modelContext) {
this.sink = requireNonNull(sink);
this.modelContext = requireNonNull(modelContext);
}
EncodingName.RFC8040_JSON, JSONNotificationFormatter.FACTORY,
EncodingName.RFC8040_XML, XMLNotificationFormatter.FACTORY);
- AbstractNotificationSource() {
+ protected AbstractNotificationSource() {
super(ENCODINGS);
}
}
--- /dev/null
+/*
+ * 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.streams.notif;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableSet;
+import java.net.URI;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.opendaylight.mdsal.dom.api.DOMNotificationService;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscriptionOutput;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateNotificationStream;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateNotificationStreamInput;
+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.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * RESTCONF implementation of {@link CreateNotificationStream}.
+ */
+@Singleton
+@Component
+public final class CreateNotificationStreamRpc extends RpcImplementation {
+ private static final NodeIdentifier SAL_REMOTE_OUTPUT_NODEID =
+ NodeIdentifier.create(CreateDataChangeEventSubscriptionOutput.QNAME);
+ private static final NodeIdentifier NOTIFICATIONS =
+ NodeIdentifier.create(QName.create(CreateNotificationStreamInput.QNAME, "notifications").intern());
+ private static final NodeIdentifier STREAM_NAME_NODEID =
+ NodeIdentifier.create(QName.create(CreateDataChangeEventSubscriptionOutput.QNAME, "stream-name").intern());
+
+ private final DatabindProvider databindProvider;
+ private final DOMNotificationService notificationService;
+ private final RestconfStream.Registry streamRegistry;
+
+ @Inject
+ @Activate
+ public CreateNotificationStreamRpc(@Reference final RestconfStream.Registry streamRegistry,
+ @Reference final DatabindProvider databindProvider,
+ @Reference final DOMNotificationService notificationService) {
+ super(CreateNotificationStream.QNAME);
+ this.databindProvider = requireNonNull(databindProvider);
+ this.notificationService = requireNonNull(notificationService);
+ this.streamRegistry = requireNonNull(streamRegistry);
+ }
+
+ @Override
+ public RestconfFuture<OperationOutput> invoke(final URI restconfURI, final OperationInput input) {
+ final var body = input.input();
+ final var qnames = ((LeafSetNode<String>) body.getChildByArg(NOTIFICATIONS)).body().stream()
+ .map(LeafSetEntryNode::body)
+ .map(QName::create)
+ .sorted()
+ .collect(ImmutableSet.toImmutableSet());
+
+ final var modelContext = input.currentContext().modelContext();
+ final var description = new StringBuilder("YANG notifications matching any of {");
+ var haveFirst = false;
+ for (var qname : qnames) {
+ final var module = modelContext.findModuleStatement(qname.getModule())
+ .orElseThrow(() -> new RestconfDocumentedException(qname + " refers to an unknown module",
+ ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
+ final var stmt = module.findSchemaTreeNode(qname)
+ .orElseThrow(() -> new RestconfDocumentedException(qname + " refers to an unknown notification",
+ ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
+ if (!(stmt instanceof NotificationEffectiveStatement)) {
+ throw new RestconfDocumentedException(qname + " refers to a non-notification",
+ ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+ }
+
+ if (haveFirst) {
+ description.append(",\n");
+ } else {
+ haveFirst = true;
+ }
+ description.append("\n ")
+ .append(module.argument().getLocalName()).append(':').append(qname.getLocalName());
+ }
+ description.append("\n}");
+
+ return streamRegistry.createStream(restconfURI,
+ new NotificationSource(databindProvider, notificationService, qnames), description.toString())
+ .transform(stream -> input.newOperationOutput(Builders.containerBuilder()
+ .withNodeIdentifier(SAL_REMOTE_OUTPUT_NODEID)
+ .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
+ .build()));
+ }
+}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.stream.JsonWriter;
import javax.xml.xpath.XPathExpressionException;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.$YangModuleInfoImpl;
import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
}
@Override
- String createText(final TextParameters params, final EffectiveModelContext schemaContext,
+ protected String createText(final TextParameters params, final EffectiveModelContext schemaContext,
final DOMNotification input, final Instant now) throws IOException {
try (var writer = new StringWriter()) {
try (var jsonWriter = new JsonWriter(writer)) {
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import java.io.IOException;
import java.time.Instant;
import javax.xml.xpath.XPathExpressionException;
import org.opendaylight.mdsal.dom.api.DOMEvent;
import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.server.spi.EventFormatter;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateNotificationStream;
import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
}
@Override
- final void fillDocument(final Document doc, final EffectiveModelContext schemaContext, final DOMNotification input)
- throws IOException {
+ protected final void fillDocument(final Document doc, final EffectiveModelContext schemaContext,
+ final DOMNotification input) throws IOException {
final var notificationElement = createNotificationElement(doc,
input instanceof DOMEvent domEvent ? domEvent.getEventInstant() : Instant.now());
// FIXME: what is this really?!
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.server.spi.EventFormatter;
+import org.opendaylight.restconf.server.spi.EventFormatterFactory;
abstract class NotificationFormatterFactory extends EventFormatterFactory<DOMNotification> {
NotificationFormatterFactory(final EventFormatter<DOMNotification> emptyFormatter) {
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import static java.util.Objects.requireNonNull;
import org.opendaylight.mdsal.dom.api.DOMNotification;
import org.opendaylight.mdsal.dom.api.DOMNotificationService;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Sink;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.Source;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sink;
+import org.opendaylight.restconf.server.spi.RestconfStream.Source;
import org.opendaylight.yangtools.concepts.Registration;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
/**
* A {@link Source} reporting YANG notifications.
*/
-public final class NotificationSource extends AbstractNotificationSource {
+final class NotificationSource extends AbstractNotificationSource {
private final DatabindProvider databindProvider;
private final DOMNotificationService notificationService;
private final ImmutableSet<QName> qnames;
this.qnames = requireNonNull(qnames);
}
- /**
- * Return notification QNames.
- *
- * @return The YANG notification {@link QName}s this listener is bound to
- */
- public ImmutableSet<QName> qnames() {
- return qnames;
- }
-
@Override
protected Registration start(final Sink<DOMNotification> sink) {
return notificationService.registerNotificationListener(
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import javax.xml.stream.XMLStreamException;
import javax.xml.xpath.XPathExpressionException;
import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.server.spi.TextParameters;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
}
@Override
- String createText(final TextParameters params, final EffectiveModelContext schemaContext,
+ protected String createText(final TextParameters params, final EffectiveModelContext schemaContext,
final DOMNotification input, final Instant now) throws IOException {
final var writer = new StringWriter();
--- /dev/null
+/*
+ * 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
+ */
+/**
+ * Support for YANG 1.0 notification streams.
+ */
+package org.opendaylight.restconf.server.mdsal.streams.notif;
\ No newline at end of file
--- /dev/null
+/*
+ * 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.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+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.URLConstants;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.Source;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.Stream;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.stream.Access;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Reference base class for {@link RestconfStream.Registry} implementations.
+ */
+public abstract class AbstractRestconfStreamRegistry implements RestconfStream.Registry {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractRestconfStreamRegistry.class);
+
+ @VisibleForTesting
+ public static final QName NAME_QNAME = QName.create(Stream.QNAME, "name").intern();
+ @VisibleForTesting
+ public static final QName DESCRIPTION_QNAME = QName.create(Stream.QNAME, "description").intern();
+ @VisibleForTesting
+ public static final QName ENCODING_QNAME = QName.create(Stream.QNAME, "encoding").intern();
+ @VisibleForTesting
+ public static final QName LOCATION_QNAME = QName.create(Stream.QNAME, "location").intern();
+
+ private final ConcurrentMap<String, RestconfStream<?>> streams = new ConcurrentHashMap<>();
+ private final boolean useWebsockets;
+
+ protected AbstractRestconfStreamRegistry(final boolean useWebsockets) {
+ this.useWebsockets = useWebsockets;
+ }
+
+ @Override
+ public final @Nullable RestconfStream<?> lookupStream(final String name) {
+ return streams.get(requireNonNull(name));
+ }
+
+ @Override
+ public final <T> RestconfFuture<RestconfStream<T>> createStream(final URI restconfURI, final Source<T> source,
+ final String description) {
+ final var baseStreamLocation = baseStreamLocation(restconfURI);
+ final var stream = allocateStream(source);
+ final var name = stream.name();
+ if (description.isBlank()) {
+ throw new IllegalArgumentException("Description must be descriptive");
+ }
+
+ final var ret = new SettableRestconfFuture<RestconfStream<T>>();
+ Futures.addCallback(putStream(streamEntry(name, description, baseStreamLocation, stream.encodings())),
+ new FutureCallback<Object>() {
+ @Override
+ public void onSuccess(final Object result) {
+ LOG.debug("Stream {} added", name);
+ ret.set(stream);
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ LOG.debug("Failed to add stream {}", name, cause);
+ streams.remove(name, stream);
+ ret.setFailure(new RestconfDocumentedException("Failed to allocate stream " + name, cause));
+ }
+ }, MoreExecutors.directExecutor());
+ return ret;
+ }
+
+ private <T> RestconfStream<T> allocateStream(final Source<T> source) {
+ String name;
+ RestconfStream<T> stream;
+ do {
+ // Use Type 4 (random) UUID. While we could just use it as a plain string, be nice to observers and anchor
+ // it into UUID URN namespace as defined by RFC4122
+ name = "urn:uuid:" + UUID.randomUUID().toString();
+ stream = new RestconfStream<>(this, source, name);
+ } while (streams.putIfAbsent(name, stream) != null);
+
+ return stream;
+ }
+
+ protected abstract @NonNull ListenableFuture<?> putStream(@NonNull MapEntryNode stream);
+
+ /**
+ * Remove a particular stream and remove its entry from operational datastore.
+ *
+ * @param stream Stream to remove
+ */
+ final void removeStream(final RestconfStream<?> stream) {
+ // Defensive check to see if we are still tracking the stream
+ final var name = stream.name();
+ if (streams.get(name) != stream) {
+ LOG.warn("Stream {} does not match expected instance {}, skipping datastore update", name, stream);
+ return;
+ }
+
+ Futures.addCallback(deleteStream(NodeIdentifierWithPredicates.of(Stream.QNAME, NAME_QNAME, name)),
+ new FutureCallback<Object>() {
+ @Override
+ public void onSuccess(final Object result) {
+ LOG.debug("Stream {} removed", name);
+ streams.remove(name, stream);
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ LOG.warn("Failed to remove stream {}, operational datastore may be inconsistent", name, cause);
+ streams.remove(name, stream);
+ }
+ }, MoreExecutors.directExecutor());
+ }
+
+ protected abstract @NonNull ListenableFuture<?> deleteStream(@NonNull NodeIdentifierWithPredicates streamName);
+
+ /**
+ * Return the base location URL of the streams service based on request URI.
+ *
+ * @param restconfURI request base URI
+ * @throws IllegalArgumentException if the result would have been malformed
+ */
+ protected final @NonNull String baseStreamLocation(final URI restconfURI) {
+ var scheme = restconfURI.getScheme();
+ if (useWebsockets) {
+ scheme = switch (scheme) {
+ // Secured HTTP goes to Secured WebSockets
+ case "https" -> "wss";
+ // Unsecured HTTP and others go to unsecured WebSockets
+ default -> "ws";
+ };
+ }
+
+ try {
+ return new URI(scheme, restconfURI.getRawUserInfo(), restconfURI.getHost(), restconfURI.getPort(),
+ restconfURI.getPath() + '/' + URLConstants.STREAMS_SUBPATH, null, null)
+ .toString();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Cannot derive streams location", e);
+ }
+ }
+
+ @VisibleForTesting
+ public static final @NonNull MapEntryNode streamEntry(final String name, final String description,
+ final String baseStreamLocation, final Set<EncodingName> encodings) {
+ final var accessBuilder = Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(Access.QNAME));
+ for (var encoding : encodings) {
+ final var encodingName = encoding.name();
+ accessBuilder.withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(Access.QNAME, ENCODING_QNAME, encodingName))
+ .withChild(ImmutableNodes.leafNode(ENCODING_QNAME, encodingName))
+ .withChild(ImmutableNodes.leafNode(LOCATION_QNAME,
+ baseStreamLocation + '/' + encodingName + '/' + name))
+ .build());
+ }
+
+ return Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(Stream.QNAME, NAME_QNAME, name))
+ .withChild(ImmutableNodes.leafNode(NAME_QNAME, name))
+ .withChild(ImmutableNodes.leafNode(DESCRIPTION_QNAME, description))
+ .withChild(accessBuilder.build())
+ .build();
+ }
+}
* 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.streams;
+package org.opendaylight.restconf.server.spi;
import static java.util.Objects.requireNonNull;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.Writer;
import java.time.Instant;
DBF = f;
}
- static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory();
+ protected static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory();
private final TextParameters textParams;
private final XPathExpression filter;
- EventFormatter(final TextParameters textParams) {
+ protected EventFormatter(final TextParameters textParams) {
this.textParams = requireNonNull(textParams);
filter = null;
}
- EventFormatter(final TextParameters params, final String xpathFilter) throws XPathExpressionException {
+ protected EventFormatter(final TextParameters params, final String xpathFilter) throws XPathExpressionException {
textParams = requireNonNull(params);
final XPath xpath;
filter = xpath.compile(xpathFilter);
}
- final @Nullable String eventData(final EffectiveModelContext schemaContext, final T input, final Instant now)
- throws Exception {
+ @VisibleForTesting
+ public final @Nullable String eventData(final EffectiveModelContext schemaContext, final T input,
+ final Instant now) throws Exception {
return filterMatches(schemaContext, input, now) ? createText(textParams, schemaContext, input, now) : null;
}
* @param input data to export
* @throws IOException if any IOException occurs during export to the document
*/
- abstract void fillDocument(Document doc, EffectiveModelContext schemaContext, T input) throws IOException;
+ protected abstract void fillDocument(Document doc, EffectiveModelContext schemaContext, T input) throws IOException;
/**
* Format the input data into string representation of the data provided.
* @return String representation of the formatted data
* @throws Exception if the underlying formatters fail to export the data to the requested format
*/
- abstract String createText(TextParameters params, EffectiveModelContext schemaContext, T input, Instant now)
- throws Exception;
+ protected abstract String createText(TextParameters params, EffectiveModelContext schemaContext, T input,
+ Instant now) throws Exception;
private boolean filterMatches(final EffectiveModelContext schemaContext, final T input, final Instant now)
throws IOException {
* @param now time stamp
* @return Data specified by RFC3339.
*/
- static final String toRFC3339(final Instant now) {
+ protected static final String toRFC3339(final Instant now) {
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(OffsetDateTime.ofInstant(now, ZoneId.systemDefault()));
}
- static final @NonNull Element createNotificationElement(final Document doc, final Instant now) {
+ protected static final @NonNull Element createNotificationElement(final Document doc, final Instant now) {
final var notificationElement = doc.createElementNS(NamespaceURN.NOTIFICATION, "notification");
final var eventTimeElement = doc.createElement("eventTime");
eventTimeElement.setTextContent(toRFC3339(now));
return notificationElement;
}
- static final @NonNull XMLStreamWriter createStreamWriterWithNotification(final Writer writer, final Instant now)
- throws XMLStreamException {
+ protected static final @NonNull XMLStreamWriter createStreamWriterWithNotification(final Writer writer,
+ final Instant now) throws XMLStreamException {
final var xmlStreamWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(writer);
xmlStreamWriter.setDefaultNamespace(NamespaceURN.NOTIFICATION);
return xmlStreamWriter;
}
- static final void writeBody(final NormalizedNodeStreamWriter writer, final NormalizedNode body) throws IOException {
+ protected static final void writeBody(final NormalizedNodeStreamWriter writer, final NormalizedNode body)
+ throws IOException {
try (var nodeWriter = NormalizedNodeWriter.forStreamWriter(writer)) {
nodeWriter.write(body);
}
* 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.streams;
+package org.opendaylight.restconf.server.spi;
import static java.util.Objects.requireNonNull;
--- /dev/null
+/*
+ * 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.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
+
+/**
+ * Input to an operation invocation.
+ */
+@NonNullByDefault
+public record OperationInput(DatabindContext currentContext, Inference operation, ContainerNode input)
+ implements DatabindProvider {
+ public OperationInput {
+ requireNonNull(currentContext);
+ requireNonNull(operation);
+ requireNonNull(input);
+ }
+
+ /**
+ * Create an {@link OperationOutput} with equal {@link #currentContext()} and {@link #operation()}.
+ *
+ * @param output Output payload
+ * @return An {@link OperationOutput}
+ */
+ public OperationOutput newOperationOutput(final @Nullable ContainerNode output) {
+ return new OperationOutput(currentContext, operation, output);
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * 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.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
+
+/**
+ * Output of {@link RpcImplementation#invoke(java.net.URI, OperationInput)}.
+ */
+public record OperationOutput(
+ @NonNull DatabindContext currentContext,
+ @NonNull Inference operation,
+ @Nullable ContainerNode output) implements DatabindProvider {
+ public OperationOutput {
+ requireNonNull(currentContext);
+ requireNonNull(operation);
+ if (output != null && output.isEmpty()) {
+ output = null;
+ }
+ }
+}
\ No newline at end of file
* 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.streams;
+package org.opendaylight.restconf.server.spi;
import static java.util.Objects.requireNonNull;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
+import java.net.URI;
import java.time.Instant;
import java.util.Set;
import java.util.regex.Pattern;
import org.checkerframework.checker.lock.qual.GuardedBy;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.stream.Access;
import org.opendaylight.yangtools.concepts.Registration;
}
}
+ /**
+ * Interface for session handler that is responsible for sending of data over established session.
+ */
+ public interface Sender {
+ /**
+ * Interface for sending String message through one of implementation.
+ *
+ * @param data Message data to be send.
+ */
+ void sendDataMessage(String data);
+
+ /**
+ * Called when the stream has reached its end. The handler should close all underlying resources.
+ */
+ void endOfStream();
+ }
+
+ /**
+ * An entity managing allocation and lookup of {@link RestconfStream}s.
+ */
+ public interface Registry {
+ /**
+ * Get a {@link RestconfStream} by its name.
+ *
+ * @param name Stream name.
+ * @return A {@link RestconfStream}, or {@code null} if the stream with specified name does not exist.
+ * @throws NullPointerException if {@code name} is {@code null}
+ */
+ @Nullable RestconfStream<?> lookupStream(String name);
+
+ /**
+ * Create a {@link RestconfStream} with a unique name. This method will atomically generate a stream name,
+ * create the corresponding instance and register it.
+ *
+ * @param <T> Stream type
+ * @param restconfURI resolved {@code {+restconf}} resource name
+ * @param source Stream instance
+ * @param description Stream descriptiion
+ * @return A future {@link RestconfStream} instance
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ <T> @NonNull RestconfFuture<RestconfStream<T>> createStream(URI restconfURI, Source<T> source,
+ String description);
+ }
+
private static final Logger LOG = LoggerFactory.getLogger(RestconfStream.class);
private static final VarHandle SUBSCRIBERS;
}
}
};
- private final @NonNull ListenersBroker listenersBroker;
+ private final @NonNull AbstractRestconfStreamRegistry registry;
private final @NonNull Source<T> source;
private final @NonNull String name;
@GuardedBy("this")
private Registration registration;
- RestconfStream(final ListenersBroker listenersBroker, final Source<T> source, final String name) {
- this.listenersBroker = requireNonNull(listenersBroker);
+ RestconfStream(final AbstractRestconfStreamRegistry registry, final Source<T> source, final String name) {
+ this.registry = requireNonNull(registry);
this.source = requireNonNull(source);
this.name = requireNonNull(name);
}
}
/**
- * Registers {@link StreamSessionHandler} subscriber.
+ * Registers {@link Sender} subscriber.
*
* @param handler SSE or WS session handler.
* @param encoding Requested event stream encoding
* @throws NullPointerException if any argument is {@code null}
* @throws UnsupportedEncodingException if {@code encoding} is not supported
* @throws XPathExpressionException if requested filter is not valid
- * @throws InvalidArgumentException if the parameters are not supported
*/
- @Nullable Registration addSubscriber(final StreamSessionHandler handler, final EncodingName encoding,
+ public @Nullable Registration addSubscriber(final Sender handler, final EncodingName encoding,
final ReceiveEventsParams params) throws UnsupportedEncodingException, XPathExpressionException {
final var factory = source.encodings.get(requireNonNull(encoding));
if (factory == null) {
registration = null;
}
}
- listenersBroker.removeStream(this);
+ registry.removeStream(this);
}
@Override
--- /dev/null
+/*
+ * 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.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import java.net.URI;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+
+/**
+ * An implementation of a YANG-defined RPC.
+ */
+@NonNullByDefault
+public abstract class RpcImplementation {
+ private final QName qname;
+
+ protected RpcImplementation(final QName qname) {
+ this.qname = requireNonNull(qname);
+ }
+
+ /**
+ * Return the RPC name, as defined by {@code rpc} statement's argument.
+ *
+ * @return The RPC name
+ */
+ public final QName qname() {
+ return qname;
+ }
+
+ /**
+ * Asynchronously invoke this implementation. Implementations are expected to report all results via the returned
+ * future, e.g. not throw exceptions.
+ *
+ * @param restconfURI Request URI trimmed to the root RESTCONF endpoint, resolved {@code {+restconf}} resource name
+ * @param input RPC input
+ * @return Future RPC output
+ */
+ public abstract RestconfFuture<OperationOutput> invoke(URI restconfURI, OperationInput input);
+
+ @Override
+ public final String toString() {
+ return MoreObjects.toStringHelper(this).add("qname", qname).toString();
+ }
+
+ protected static final <T> @Nullable T leaf(final ContainerNode parent, final NodeIdentifier arg,
+ final Class<T> type) {
+ final var child = parent.childByArg(arg);
+ if (child instanceof LeafNode<?> leafNode) {
+ final var body = leafNode.body();
+ try {
+ return type.cast(body);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException("Bad child " + child.prettyTree(), e);
+ }
+ }
+ return null;
+ }
+}
* 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.streams;
+package org.opendaylight.restconf.server.spi;
import static java.util.Objects.requireNonNull;
import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sender;
import org.opendaylight.yangtools.concepts.AbstractRegistration;
/**
*/
final class Subscriber<T> extends AbstractRegistration {
private final @NonNull RestconfStream<T> stream;
- private final @NonNull StreamSessionHandler handler;
+ private final @NonNull Sender sender;
private final @NonNull EventFormatter<T> formatter;
- Subscriber(final RestconfStream<T> stream, final StreamSessionHandler handler, final EventFormatter<T> formatter) {
+ Subscriber(final RestconfStream<T> stream, final Sender sender, final EventFormatter<T> formatter) {
this.stream = requireNonNull(stream);
- this.handler = requireNonNull(handler);
+ this.sender = requireNonNull(sender);
this.formatter = requireNonNull(formatter);
}
return formatter;
}
- @NonNull StreamSessionHandler handler() {
- return handler;
+ @NonNull Sender sender() {
+ return sender;
}
@Override
* 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.streams;
+package org.opendaylight.restconf.server.spi;
import static java.util.Objects.requireNonNull;
@Override
void endOfStream() {
- subscriber.handler().endOfStream();
+ subscriber.sender().endOfStream();
}
@Override
void publish(final EffectiveModelContext modelContext, final T input, final Instant now) {
final var formatted = format(subscriber.formatter(), modelContext, input, now);
if (formatted != null) {
- subscriber.handler().sendDataMessage(formatted);
+ subscriber.sender().sendDataMessage(formatted);
}
}
}
@Override
void endOfStream() {
- subscribers.forEach((formatter, subscriber) -> subscriber.handler().endOfStream());
+ subscribers.forEach((formatter, subscriber) -> subscriber.sender().endOfStream());
}
@Override
final var formatted = format(entry.getKey(), modelContext, input, now);
if (formatted != null) {
for (var subscriber : entry.getValue()) {
- subscriber.handler().sendDataMessage(formatted);
+ subscriber.sender().sendDataMessage(formatted);
}
}
}
* @return An empty {@link Subscribers} file
*/
@SuppressWarnings("unchecked")
- static <T> @NonNull Subscribers<T> empty() {
+ static <T> org.opendaylight.restconf.server.spi.Subscribers<T> empty() {
return (Subscribers<T>) Empty.INSTANCE;
}
* 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.streams;
+package org.opendaylight.restconf.server.spi;
-import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.NonNull;
/**
* Text formatting parameters.
* changed nodes
* @param childNodesOnly {@code true} if this query should only notify about child node changes
*/
-@NonNullByDefault
-record TextParameters(boolean leafNodesOnly, boolean skipData, boolean changedLeafNodesOnly, boolean childNodesOnly) {
- static final TextParameters EMPTY = new TextParameters(false, false, false, false);
+public record TextParameters(
+ boolean leafNodesOnly,
+ boolean skipData,
+ boolean changedLeafNodesOnly,
+ boolean childNodesOnly) {
+ public static final @NonNull TextParameters EMPTY = new TextParameters(false, false, false, false);
}
\ No newline at end of file
--- /dev/null
+/*
+ * 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
+ */
+/**
+ * Interface towards RestconfServer implementations.
+ */
+package org.opendaylight.restconf.server.spi;
\ No newline at end of file
+++ /dev/null
-/*
- * Copyright (c) 2022 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;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
-
-public class CapabilitiesWriterTest {
- @Test
- public void restconfStateCapabilitiesTest() {
- final var capability = CapabilitiesWriter.mapCapabilities();
- assertEquals(CapabilitiesWriter.CAPABILITY, capability.name());
-
- assertThat(capability.body().stream().map(LeafSetEntryNode::body).toList(),
- containsInAnyOrder(
- equalTo("urn:ietf:params:restconf:capability:depth:1.0"),
- equalTo("urn:ietf:params:restconf:capability:fields:1.0"),
- equalTo("urn:ietf:params:restconf:capability:filter:1.0"),
- equalTo("urn:ietf:params:restconf:capability:replay:1.0"),
- equalTo("urn:ietf:params:restconf:capability:with-defaults:1.0"),
- equalTo("urn:opendaylight:params:restconf:capability:pretty-print:1.0"),
- equalTo("urn:opendaylight:params:restconf:capability:leaf-nodes-only:1.0"),
- equalTo("urn:opendaylight:params:restconf:capability:changed-leaf-nodes-only:1.0"),
- equalTo("urn:opendaylight:params:restconf:capability:skip-notification-data:1.0"),
- equalTo("urn:opendaylight:params:restconf:capability:child-nodes-only:1.0")));
- }
-}
import java.util.Optional;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.NetconfRestconfStrategy;
import org.opendaylight.yangtools.yang.common.ErrorTag;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class MdsalRestconfServerTest extends AbstractJukeboxTest {
+ private static DatabindProvider DATABIND_PROVIDER;
+
@Mock
private DOMMountPointService mountPointService;
@Mock
private MdsalRestconfServer server;
+ @BeforeClass
+ public static void setupDatabind() {
+ DATABIND_PROVIDER = () -> DatabindContext.ofModel(JUKEBOX_SCHEMA);
+ }
+
@Before
public void before() {
- server = new MdsalRestconfServer(dataBroker, rpcService, mountPointService);
+ server = new MdsalRestconfServer(DATABIND_PROVIDER, dataBroker, rpcService, mountPointService);
doReturn(Optional.of(rpcService)).when(mountPoint).getService(DOMRpcService.class);
}
import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
Builders.containerBuilder().withNodeIdentifier(NodeIdentifier.create(OUTPUT_QNAME)).build())))
.when(actionService).invokeAction(eq(Absolute.of(CONT_QNAME, CONT1_QNAME, RESET_QNAME)), any(), any());
- final var dataService = new RestconfDataServiceImpl(() -> DatabindContext.ofModel(IID_SCHEMA),
- new MdsalRestconfServer(dataBroker, rpcService, mountPointService), actionService);
+ final DatabindProvider databindProvider = () -> DatabindContext.ofModel(IID_SCHEMA);
+ final var dataService = new RestconfDataServiceImpl(databindProvider,
+ new MdsalRestconfServer(databindProvider, dataBroker, rpcService, mountPointService), actionService);
doReturn(true).when(asyncResponse).resume(captor.capture());
dataService.postDataJSON("instance-identifier-module:cont/cont1/reset",
import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
import org.opendaylight.yangtools.yang.common.ErrorTag;
doReturn(read).when(dataBroker).newReadOnlyTransaction();
doReturn(readWrite).when(dataBroker).newReadWriteTransaction();
- dataService = new RestconfDataServiceImpl(() -> DatabindContext.ofModel(JUKEBOX_SCHEMA),
- new MdsalRestconfServer(dataBroker, rpcService, mountPointService), actionService);
+ final DatabindProvider databindProvider = () -> DatabindContext.ofModel(JUKEBOX_SCHEMA);
+ dataService = new RestconfDataServiceImpl(databindProvider,
+ new MdsalRestconfServer(databindProvider, dataBroker, rpcService, mountPointService), actionService);
doReturn(Optional.of(mountPoint)).when(mountPointService)
.getMountPoint(any(YangInstanceIdentifier.class));
doReturn(Optional.of(FixedDOMSchemaService.of(JUKEBOX_SCHEMA))).when(mountPoint)
import com.google.common.util.concurrent.Futures;
import java.io.ByteArrayInputStream;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
-import org.opendaylight.restconf.nb.rfc8040.streams.ListenersBroker;
+import org.opendaylight.restconf.server.spi.OperationInput;
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.data.api.schema.ContainerNode;
import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RestconfInvokeOperationsServiceImplTest {
- private static final QName RPC = QName.create("ns", "2015-02-28", "test-rpc");
+ private static final URI RESTCONF_URI = URI.create("/restconf");
+ private static final QName RPC = QName.create("invoke:rpc:module", "2013-12-03", "rpc-test");
private static final ContainerNode INPUT = Builders.containerBuilder()
.withNodeIdentifier(new NodeIdentifier(QName.create(RPC, "input")))
.withChild(ImmutableNodes.leafNode(QName.create(RPC, "content"), "test"))
.withNodeIdentifier(new NodeIdentifier(QName.create(RPC, "output")))
.withChild(ImmutableNodes.leafNode(QName.create(RPC, "content"), "operation result"))
.build();
-
private static final DatabindContext CONTEXT =
DatabindContext.ofModel(YangParserTestUtils.parseYangResourceDirectory("/invoke-rpc"));
+ private static final OperationInput OPER_INPUT = new OperationInput(CONTEXT,
+ SchemaInferenceStack.of(CONTEXT.modelContext(), Absolute.of(RPC)).toInference(), INPUT);
@Mock
private DOMDataBroker dataBroker;
@Before
public void setup() {
- server = new MdsalRestconfServer(dataBroker, rpcService, mountPointService);
- invokeOperationsService = new RestconfInvokeOperationsServiceImpl(() -> CONTEXT, server,
- new ListenersBroker.WebSockets(dataBroker, notificationService, mountPointService));
+ server = new MdsalRestconfServer(() -> CONTEXT, dataBroker, rpcService, mountPointService);
+ invokeOperationsService = new RestconfInvokeOperationsServiceImpl(server);
}
@Test
public void invokeRpcTest() throws Exception {
doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of()))).when(rpcService)
.invokeRpc(RPC, INPUT);
- assertEquals(Optional.of(OUTPUT), Futures.getDone(server.getRestconfStrategy(CONTEXT.modelContext(), null)
- .invokeRpc(RPC, INPUT)));
+ assertEquals(OUTPUT,
+ Futures.getDone(
+ server.getRestconfStrategy(CONTEXT.modelContext(), null).invokeRpc(RESTCONF_URI, RPC, OPER_INPUT))
+ .output());
}
@Test
"No implementation of RPC " + errorRpc + " available.");
doReturn(Futures.immediateFailedFuture(exception)).when(rpcService).invokeRpc(errorRpc, INPUT);
final var ex = assertInstanceOf(RestconfDocumentedException.class,
- assertThrows(ExecutionException.class, () -> Futures.getDone(
- server.getRestconfStrategy(CONTEXT.modelContext(), null).invokeRpc(errorRpc, INPUT))).getCause());
+ assertThrows(ExecutionException.class,
+ () -> Futures.getDone(server.getRestconfStrategy(CONTEXT.modelContext(), null)
+ .invokeRpc(RESTCONF_URI, errorRpc, OPER_INPUT))).getCause());
final var errorList = ex.getErrors();
assertEquals(1, errorList.size());
final var actual = errorList.iterator().next();
doReturn(Optional.of(dataBroker)).when(mountPoint).getService(DOMDataBroker.class);
doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of()))).when(rpcService)
.invokeRpc(RPC, INPUT);
- assertEquals(Optional.of(OUTPUT), Futures.getDone(
- server.getRestconfStrategy(CONTEXT.modelContext(), mountPoint).invokeRpc(RPC, INPUT)));
+ assertEquals(OUTPUT,
+ Futures.getDone(
+ server.getRestconfStrategy(CONTEXT.modelContext(), mountPoint).invokeRpc(RESTCONF_URI, RPC, OPER_INPUT))
+ .output());
}
@Test
doReturn(Optional.of(dataBroker)).when(mountPoint).getService(DOMDataBroker.class);
final var strategy = server.getRestconfStrategy(CONTEXT.modelContext(), mountPoint);
final var ex = assertInstanceOf(RestconfDocumentedException.class,
- assertThrows(ExecutionException.class, () -> Futures.getDone(strategy.invokeRpc(RPC, INPUT))).getCause());
+ assertThrows(ExecutionException.class,
+ () -> Futures.getDone(strategy.invokeRpc(RESTCONF_URI, RPC, OPER_INPUT))).getCause());
final var errors = ex.getErrors();
assertEquals(1, errors.size());
final var error = errors.get(0);
public void checkResponseTest() throws Exception {
doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of())))
.when(rpcService).invokeRpc(RPC, INPUT);
- assertEquals(Optional.of(OUTPUT), Futures.getDone(
- server.getRestconfStrategy(CONTEXT.modelContext(), null).invokeRpc(RPC, INPUT)));
+ assertEquals(OUTPUT,
+ Futures.getDone(server.getRestconfStrategy(CONTEXT.modelContext(), null)
+ .invokeRpc(RESTCONF_URI, RPC, OPER_INPUT))
+ .output());
}
private void prepNNC(final ContainerNode result) {
import org.opendaylight.mdsal.dom.api.DOMRpcService;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.yang.gen.v1.module._1.rev140101.Module1Data;
import org.opendaylight.yang.gen.v1.module._2.rev140102.Module2Data;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
doReturn(Optional.of(schemaService)).when(mountPoint).getService(DOMSchemaService.class);
doReturn(Optional.of(mountPoint)).when(mountPointService).getMountPoint(any());
- opService = new RestconfOperationsServiceImpl(() -> DatabindContext.ofModel(SCHEMA),
- new MdsalRestconfServer(dataBroker, rpcService, mountPointService));
+ final DatabindProvider databindProvider = () -> DatabindContext.ofModel(SCHEMA);
+ opService = new RestconfOperationsServiceImpl(
+ new MdsalRestconfServer(databindProvider, dataBroker, rpcService, mountPointService));
}
@Test
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
public abstract class AbstractNotificationListenerTest {
- static final QNameModule MODULE = QNameModule.create(XMLNamespace.of("notifi:mod"), Revision.of("2016-11-23"));
-
+ protected static final QNameModule MODULE =
+ QNameModule.create(XMLNamespace.of("notifi:mod"), Revision.of("2016-11-23"));
protected static final EffectiveModelContext MODEL_CONTEXT =
YangParserTestUtils.parseYangResourceDirectory("/notifications");
}
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
import com.google.common.util.concurrent.Uninterruptibles;
import java.io.IOException;
+import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.opendaylight.mdsal.binding.dom.adapter.test.AbstractConcurrentDataBrokerTest;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
-import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.mdsal.dom.api.DOMNotificationService;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService;
import org.opendaylight.restconf.api.query.ChangedLeafNodesOnlyParam;
import org.opendaylight.restconf.api.query.ChildNodesOnlyParam;
import org.opendaylight.restconf.api.query.LeafNodesOnlyParam;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.mdsal.MdsalRestconfStreamRegistry;
+import org.opendaylight.restconf.server.mdsal.streams.dtcl.DataTreeChangeSource;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.Sender;
import org.opendaylight.yang.gen.v1.augment.instance.identifier.patch.module.rev220218.PatchCont1Builder;
import org.opendaylight.yang.gen.v1.augment.instance.identifier.patch.module.rev220218.patch.cont.patch.choice1.PatchCase1Builder;
import org.opendaylight.yang.gen.v1.augment.instance.identifier.patch.module.rev220218.patch.cont.patch.choice2.PatchCase11Builder;
import org.xmlunit.assertj.XmlAssert;
public class DataTreeChangeStreamTest extends AbstractConcurrentDataBrokerTest {
- private static final class TestHandler implements StreamSessionHandler {
+ private static final class TestHandler implements Sender {
private CountDownLatch notificationLatch = new CountDownLatch(1);
private volatile String lastNotification;
private DataBroker dataBroker;
private DOMDataBroker domDataBroker;
private DatabindProvider databindProvider;
- private ListenersBroker listenersBroker;
+ private RestconfStream.Registry streamRegistry;
@BeforeClass
public static void beforeClass() {
dataBroker = getDataBroker();
domDataBroker = getDomBroker();
databindProvider = () -> DatabindContext.ofModel(SCHEMA_CONTEXT);
- listenersBroker = new ListenersBroker.ServerSentEvents(domDataBroker, mock(DOMNotificationService.class),
- mock(DOMMountPointService.class));
+ streamRegistry = new MdsalRestconfStreamRegistry(domDataBroker);
}
TestHandler createHandler(final YangInstanceIdentifier path, final String streamName,
final NotificationOutputType outputType, final boolean leafNodesOnly, final boolean skipNotificationData,
final boolean changedLeafNodesOnly, final boolean childNodesOnly) throws Exception {
- final var stream = listenersBroker.createStream("test", "baseURI",
- new DataTreeChangeSource(databindProvider, domDataBroker, LogicalDatastoreType.CONFIGURATION, path))
+ final var stream = streamRegistry.createStream(URI.create("baseURI"),
+ new DataTreeChangeSource(databindProvider,
+ domDataBroker.getExtensions().getInstance(DOMDataTreeChangeService.class),
+ LogicalDatastoreType.CONFIGURATION, path), "test")
.getOrThrow();
final var handler = new TestHandler();
stream.addSubscriber(handler,
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
import org.opendaylight.yangtools.concepts.Registration;
@ExtendWith(MockitoExtension.class)
class SSESessionHandlerTest {
@Mock
- private ScheduledExecutorService executorService;
+ private PingExecutor pingExecutor;
@Mock
private RestconfStream<?> stream;
@Mock
- private ScheduledFuture<?> pingFuture;
+ private Registration pingRegistration;
@Mock
private SseEventSink eventSink;
@Mock
@Mock
private Registration reg;
- private SSESessionHandler setup(final int maxFragmentSize, final int heartbeatInterval) throws Exception {
- final var sseSessionHandler = new SSESessionHandler(executorService, eventSink, sse, stream,
+ private SSESender setup(final int maxFragmentSize, final long heartbeatInterval) throws Exception {
+ final var sseSessionHandler = new SSESender(pingExecutor, eventSink, sse, stream,
EncodingName.RFC8040_XML, new ReceiveEventsParams(null, null, null, null, null, null, null),
maxFragmentSize, heartbeatInterval);
doReturn(reg).when(stream).addSubscriber(eq(sseSessionHandler), any(), any());
}
private void setupPing(final long maxFragmentSize, final long heartbeatInterval) {
- doReturn(pingFuture).when(executorService)
- .scheduleWithFixedDelay(any(Runnable.class), eq(heartbeatInterval), eq(heartbeatInterval),
- eq(TimeUnit.MILLISECONDS));
+ doReturn(pingRegistration).when(pingExecutor)
+ .startPingProcess(any(Runnable.class), eq(heartbeatInterval), eq(TimeUnit.MILLISECONDS));
}
@Test
void onSSEConnectedWithEnabledPing() throws Exception {
- final int heartbeatInterval = 1000;
+ final var heartbeatInterval = 1000L;
final var sseSessionHandler = setup(1000, heartbeatInterval);
sseSessionHandler.init();
- verify(executorService).scheduleWithFixedDelay(any(Runnable.class), eq((long) heartbeatInterval),
- eq((long) heartbeatInterval), eq(TimeUnit.MILLISECONDS));
+ verify(pingExecutor).startPingProcess(any(Runnable.class), eq(heartbeatInterval), eq(TimeUnit.MILLISECONDS));
}
@Test
final var sseSessionHandler = setup(1000, heartbeatInterval);
sseSessionHandler.init();
- verifyNoMoreInteractions(executorService);
+ verifyNoMoreInteractions(pingExecutor);
}
@Test
final var sseSessionHandler = setup(150, 8000);
setupPing(150, 8000);
sseSessionHandler.init();
- doReturn(false).when(pingFuture).isCancelled();
- doReturn(false).when(pingFuture).isDone();
+ doNothing().when(pingRegistration).close();
sseSessionHandler.close();
verify(reg).close();
- verify(pingFuture).cancel(anyBoolean());
}
@Test
setupPing(150, 8000);
sseSessionHandler.init();
+ doNothing().when(pingRegistration).close();
sseSessionHandler.close();
verify(reg).close();
- verify(pingFuture).cancel(anyBoolean());
}
@Test
sseSessionHandler.close();
verify(reg).close();
- verify(pingFuture, never()).cancel(anyBoolean());
+ verifyNoMoreInteractions(pingRegistration);
}
@Test
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
-import com.google.common.collect.ImmutableClassToInstanceMap;
import java.net.URI;
-import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.junit.jupiter.api.BeforeEach;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMNotificationService;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.restconf.server.mdsal.MdsalRestconfStreamRegistry;
+import org.opendaylight.restconf.server.mdsal.streams.dtcl.DataTreeChangeSource;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
private static final QName TOASTER = QName.create("http://netconfcentral.org/ns/toaster", "2009-11-20", "toaster");
@Mock
- private ScheduledExecutorService execService;
+ private PingExecutor pingExecutor;
@Mock
private ServletUpgradeRequest upgradeRequest;
@Mock
@Mock
private DOMDataBroker dataBroker;
@Mock
- private DOMDataTreeChangeService changeService;
- @Mock
private DOMDataTreeWriteTransaction tx;
@Mock
+ private DOMDataTreeChangeService changeService;
+ @Mock
private DatabindProvider databindProvider;
@Mock
private DOMMountPointService mountPointService;
@Mock
private DOMNotificationService notificationService;
- private ListenersBroker listenersBroker;
private WebSocketFactory webSocketFactory;
private String streamName;
@BeforeEach
void prepareListenersBroker() {
- doReturn(ImmutableClassToInstanceMap.of(DOMDataTreeChangeService.class, changeService)).when(dataBroker)
- .getExtensions();
doReturn(tx).when(dataBroker).newWriteOnlyTransaction();
doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
- listenersBroker = new ListenersBroker.ServerSentEvents(dataBroker, notificationService, mountPointService);
- webSocketFactory = new WebSocketFactory(execService, listenersBroker, 5000, 2000);
+ final var streamRegistry = new MdsalRestconfStreamRegistry(dataBroker);
+ webSocketFactory = new WebSocketFactory(streamRegistry, pingExecutor, 5000, 2000);
- streamName = listenersBroker.createStream("description", "streams",
- new DataTreeChangeSource(databindProvider, dataBroker, LogicalDatastoreType.CONFIGURATION,
- YangInstanceIdentifier.of(TOASTER)))
+ streamName = streamRegistry.createStream(URI.create("https://localhost:8181/rests"),
+ new DataTreeChangeSource(databindProvider, changeService, LogicalDatastoreType.CONFIGURATION,
+ YangInstanceIdentifier.of(TOASTER)),
+ "description")
.getOrThrow()
.name();
}
doReturn(URI.create("https://localhost:8181/rests/streams/xml/" + streamName))
.when(upgradeRequest).getRequestURI();
- assertInstanceOf(WebSocketSessionHandler.class,
+ assertInstanceOf(WebSocketSender.class,
webSocketFactory.createWebSocket(upgradeRequest, upgradeResponse));
verify(upgradeResponse).setSuccess(true);
verify(upgradeResponse).setStatusCode(101);
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
import org.opendaylight.yangtools.concepts.Registration;
@ExtendWith(MockitoExtension.class)
class WebSocketSessionHandlerTest {
private final class WebSocketTestSessionState {
- private final WebSocketSessionHandler webSocketSessionHandler;
- private final int heartbeatInterval;
+ private final WebSocketSender webSocketSessionHandler;
+ private final long heartbeatInterval;
private final int maxFragmentSize;
- WebSocketTestSessionState(final int maxFragmentSize, final int heartbeatInterval) {
+ WebSocketTestSessionState(final int maxFragmentSize, final long heartbeatInterval) {
this.heartbeatInterval = heartbeatInterval;
this.maxFragmentSize = maxFragmentSize;
- webSocketSessionHandler = new WebSocketSessionHandler(executorService, stream,
- ENCODING, null, maxFragmentSize, heartbeatInterval);
+ webSocketSessionHandler = new WebSocketSender(pingExecutor, stream, ENCODING, null, maxFragmentSize,
+ heartbeatInterval);
if (heartbeatInterval != 0) {
- doReturn(pingFuture).when(executorService).scheduleWithFixedDelay(any(Runnable.class),
- eq((long) heartbeatInterval), eq((long) heartbeatInterval), eq(TimeUnit.MILLISECONDS));
+ doReturn(pingRegistration).when(pingExecutor).startPingProcess(any(Runnable.class),
+ eq(heartbeatInterval), eq(TimeUnit.MILLISECONDS));
}
}
}
@Mock
private RestconfStream<?> stream;
@Mock
- private ScheduledExecutorService executorService;
+ private PingExecutor pingExecutor;
@Mock
- private ScheduledFuture pingFuture;
+ private Registration pingRegistration;
@Mock
private Session session;
webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
verify(stream).addSubscriber(webSocketTestSessionState.webSocketSessionHandler, ENCODING, null);
- verify(executorService).scheduleWithFixedDelay(any(Runnable.class),
- eq((long) webSocketTestSessionState.heartbeatInterval),
- eq((long) webSocketTestSessionState.heartbeatInterval), eq(TimeUnit.MILLISECONDS));
+ verify(pingExecutor).startPingProcess(any(Runnable.class), eq(webSocketTestSessionState.heartbeatInterval),
+ eq(TimeUnit.MILLISECONDS));
}
@Test
webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
verify(stream).addSubscriber(webSocketTestSessionState.webSocketSessionHandler, ENCODING, null);
- verifyNoMoreInteractions(executorService);
+ verifyNoMoreInteractions(pingExecutor);
}
@Test
when(stream.addSubscriber(webSocketTestSessionState.webSocketSessionHandler, ENCODING, null))
.thenReturn(reg);
webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
- when(pingFuture.isCancelled()).thenReturn(false);
- when(pingFuture.isDone()).thenReturn(false);
final var sampleError = new IllegalStateException("Simulated error");
doNothing().when(reg).close();
+ doNothing().when(pingRegistration).close();
webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
- verify(reg).close();
verify(session).close();
- verify(pingFuture).cancel(anyBoolean());
}
@Test
webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
final var sampleError = new IllegalStateException("Simulated error");
+ doNothing().when(reg).close();
+ doNothing().when(pingRegistration).close();
webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
- verify(reg).close();
verify(session, never()).close();
- verify(pingFuture).cancel(anyBoolean());
}
@Test
when(stream.addSubscriber(webSocketTestSessionState.webSocketSessionHandler, ENCODING, null))
.thenReturn(reg);
webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
- when(pingFuture.isDone()).thenReturn(true);
final var sampleError = new IllegalStateException("Simulated error");
webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
verify(reg).close();
verify(session, never()).close();
- verify(pingFuture, never()).cancel(anyBoolean());
}
@Test
--- /dev/null
+/*
+ * Copyright (c) 2022 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 org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+
+class CapabilitiesWriterTest {
+ @Test
+ void restconfStateCapabilitiesTest() {
+ final var capability = CapabilitiesWriter.mapCapabilities();
+ assertEquals(CapabilitiesWriter.CAPABILITY, capability.name());
+
+ final var entries = capability.body().stream().map(LeafSetEntryNode::body).toList();
+ final var unique = Set.copyOf(entries);
+ assertEquals(Set.of(
+ "urn:ietf:params:restconf:capability:depth:1.0",
+ "urn:ietf:params:restconf:capability:fields:1.0",
+ "urn:ietf:params:restconf:capability:filter:1.0",
+ "urn:ietf:params:restconf:capability:replay:1.0",
+ "urn:ietf:params:restconf:capability:with-defaults:1.0",
+ "urn:opendaylight:params:restconf:capability:pretty-print:1.0",
+ "urn:opendaylight:params:restconf:capability:leaf-nodes-only:1.0",
+ "urn:opendaylight:params:restconf:capability:changed-leaf-nodes-only:1.0",
+ "urn:opendaylight:params:restconf:capability:skip-notification-data:1.0",
+ "urn:opendaylight:params:restconf:capability:child-nodes-only:1.0"), unique);
+ assertEquals(unique.size(), entries.size());
+ }
+}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.dtcl;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import com.google.common.collect.ImmutableClassToInstanceMap;
import java.net.URI;
import java.util.UUID;
+import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService;
import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
-import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.mdsal.dom.api.DOMNotificationService;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
+import org.opendaylight.restconf.server.mdsal.MdsalRestconfStreamRegistry;
+import org.opendaylight.restconf.server.spi.OperationInput;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.stream.Access;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscription;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.CreateDataChangeEventSubscriptionOutput;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
-import org.opendaylight.yangtools.yang.model.api.ContainerLike;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
@ExtendWith(MockitoExtension.class)
-class ListenersBrokerTest {
+class CreateNotificationStreamRpcTest {
private static final EffectiveModelContext SCHEMA_CTX = YangParserTestUtils.parseYangResourceDirectory("/streams");
- private static final URI BASE_URI = URI.create("baseURI");
+ private static final URI RESTCONF_URI = URI.create("/rests");
+ private static final YangInstanceIdentifier TOASTER = YangInstanceIdentifier.of(
+ QName.create("http://netconfcentral.org/ns/toaster", "2009-11-20", "toaster"));
@Mock
private DOMDataBroker dataBroker;
@Mock
private DOMDataTreeChangeService treeChange;
@Mock
- private DOMMountPointService mountPointService;
- @Mock
- private DOMNotificationService notificationService;
- @Mock
private DOMDataTreeWriteTransaction tx;
@Captor
private ArgumentCaptor<YangInstanceIdentifier> pathCaptor;
@Captor
private ArgumentCaptor<NormalizedNode> dataCaptor;
- private ListenersBroker listenersBroker;
private DatabindProvider databindProvider;
+ private CreateDataChangeEventSubscriptionRpc rpc;
+
@BeforeEach
public void before() {
- listenersBroker = new ListenersBroker.ServerSentEvents(dataBroker, notificationService, mountPointService);
databindProvider = () -> DatabindContext.ofModel(SCHEMA_CTX);
- }
- @Test
- void createStreamTest() {
doReturn(ImmutableClassToInstanceMap.of(DOMDataTreeChangeService.class, treeChange))
.when(dataBroker).getExtensions();
+ rpc = new CreateDataChangeEventSubscriptionRpc(new MdsalRestconfStreamRegistry(dataBroker), databindProvider,
+ dataBroker);
+ }
+ @Test
+ void createStreamTest() {
doReturn(tx).when(dataBroker).newWriteOnlyTransaction();
doNothing().when(tx).put(eq(LogicalDatastoreType.OPERATIONAL), pathCaptor.capture(), dataCaptor.capture());
doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
- final var output = assertInstanceOf(ContainerNode.class,
- listenersBroker.createDataChangeNotifiStream(databindProvider, BASE_URI,
- prepareDomPayload("create-data-change-event-subscription", "toaster", "path"), SCHEMA_CTX)
- .getOrThrow()
- .orElse(null));
+ final var output = assertInstanceOf(ContainerNode.class, rpc.invoke(RESTCONF_URI, createInput("path", TOASTER))
+ .getOrThrow().output());
assertEquals(new NodeIdentifier(CreateDataChangeEventSubscriptionOutput.QNAME), output.name());
assertEquals(1, output.size());
.withChild(Builders.mapEntryBuilder()
.withNodeIdentifier(NodeIdentifierWithPredicates.of(Access.QNAME, rcEncoding, "json"))
.withChild(ImmutableNodes.leafNode(rcEncoding, "json"))
- .withChild(ImmutableNodes.leafNode(rcLocation, "rests/streams/json/" + name))
+ .withChild(ImmutableNodes.leafNode(rcLocation, "/rests/streams/json/" + name))
.build())
.withChild(Builders.mapEntryBuilder()
.withNodeIdentifier(NodeIdentifierWithPredicates.of(Access.QNAME, rcEncoding, "xml"))
.withChild(ImmutableNodes.leafNode(rcEncoding, "xml"))
- .withChild(ImmutableNodes.leafNode(rcLocation, "rests/streams/xml/" + name))
+ .withChild(ImmutableNodes.leafNode(rcLocation, "/rests/streams/xml/" + name))
.build())
.build())
.build().prettyTree().toString(), dataCaptor.getValue().prettyTree().toString());
@Test
void createStreamWrongValueTest() {
- final var payload = prepareDomPayload("create-data-change-event-subscription", "String value", "path");
- final var errors = assertThrows(RestconfDocumentedException.class,
- () -> listenersBroker.createDataChangeNotifiStream(databindProvider, BASE_URI, payload, SCHEMA_CTX))
- .getErrors();
- assertEquals(1, errors.size());
- final var error = errors.get(0);
- assertEquals(ErrorType.APPLICATION, error.getErrorType());
- assertEquals(ErrorTag.OPERATION_FAILED, error.getErrorTag());
- assertEquals("Instance identifier was not normalized correctly", error.getErrorMessage());
+ final var payload = createInput("path", "String value");
+ final var ex = assertThrows(IllegalArgumentException.class, () -> rpc.invoke(RESTCONF_URI, payload));
+ assertEquals("""
+ Bad child leafNode (urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote@2014-01-14)path = \
+ "String value"\
+ """, ex.getMessage());
}
@Test
void createStreamWrongInputRpcTest() {
- final var payload = prepareDomPayload("create-data-change-event-subscription2", "toaster", "path2");
- final var errors = assertThrows(RestconfDocumentedException.class,
- () -> listenersBroker.createDataChangeNotifiStream(databindProvider, BASE_URI, payload, SCHEMA_CTX))
- .getErrors();
+ final var future = rpc.invoke(RESTCONF_URI, createInput(null, null));
+ final var errors = assertThrows(RestconfDocumentedException.class, future::getOrThrow).getErrors();
assertEquals(1, errors.size());
final var error = errors.get(0);
assertEquals(ErrorType.APPLICATION, error.getErrorType());
- assertEquals(ErrorTag.OPERATION_FAILED, error.getErrorTag());
- assertEquals("Instance identifier was not normalized correctly", error.getErrorMessage());
+ assertEquals(ErrorTag.MISSING_ELEMENT, error.getErrorTag());
+ assertEquals("missing path", error.getErrorMessage());
}
- private static ContainerNode prepareDomPayload(final String rpcName, final String toasterValue,
- final String inputOutputName) {
- final var rpcModule = SCHEMA_CTX.findModules("sal-remote").iterator().next();
- final QName rpcQName = QName.create(rpcModule.getQNameModule(), rpcName);
-
- ContainerLike containerSchemaNode = null;
- for (final RpcDefinition rpc : rpcModule.getRpcs()) {
- if (rpcQName.isEqualWithoutRevision(rpc.getQName())) {
- containerSchemaNode = rpc.getInput();
- break;
- }
+ private OperationInput createInput(final @Nullable String leafName, final Object leafValue) {
+ final var stack = SchemaInferenceStack.of(SCHEMA_CTX);
+ final var rpcStmt = assertInstanceOf(RpcEffectiveStatement.class,
+ stack.enterSchemaTree(CreateDataChangeEventSubscription.QNAME));
+ final var inference = stack.toInference();
+
+ final var builder = Builders.containerBuilder()
+ .withNodeIdentifier(new NodeIdentifier(rpcStmt.input().argument()));
+ if (leafName != null) {
+ final var lfQName = QName.create(rpcStmt.argument(), leafName);
+ stack.enterDataTree(rpcStmt.input().argument());
+ stack.enterDataTree(lfQName);
+ builder.withChild(ImmutableNodes.leafNode(lfQName, leafValue));
}
- assertNotNull(containerSchemaNode);
-
- final QName lfQName = QName.create(rpcModule.getQNameModule(), inputOutputName);
- assertInstanceOf(LeafSchemaNode.class, containerSchemaNode.dataChildByName(lfQName));
-
- final Object o;
- if ("toaster".equals(toasterValue)) {
- final QName rpcQname = QName.create("http://netconfcentral.org/ns/toaster", "2009-11-20", toasterValue);
- o = YangInstanceIdentifier.of(rpcQname);
- } else {
- o = toasterValue;
- }
-
- return Builders.containerBuilder()
- .withNodeIdentifier(new NodeIdentifier(containerSchemaNode.getQName()))
- .withChild(ImmutableNodes.leafNode(lfQName, o))
- .build();
+ return new OperationInput(databindProvider.currentContext(), inference, builder.build());
}
}
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.nb.rfc8040.streams.AbstractNotificationListenerTest;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
* 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.streams;
+package org.opendaylight.restconf.server.mdsal.streams.notif;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.nb.rfc8040.streams.AbstractNotificationListenerTest;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
* 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.streams;
+package org.opendaylight.restconf.server.spi;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
-import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStream.EncodingName;
+import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev190104.module.list.Module;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev190104.module.list.module.Deviation;
import org.opendaylight.yangtools.yang.common.QName;
import org.slf4j.LoggerFactory;
/**
- * Unit tests for {@link RestconfStateStreams}.
+ * Unit tests for {@link AbstractRestconfStreamRegistry}.
*/
-class RestconfStateStreamsTest {
- private static final Logger LOG = LoggerFactory.getLogger(RestconfStateStreamsTest.class);
+class AbstractRestconfStreamRegistryTest {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractRestconfStreamRegistryTest.class);
private static final EffectiveModelContext CONTEXT =
// TODO: assemble these from dependencies?
YangParserTestUtils.parseYangResourceDirectory("/modules/restconf-module-testing");
final var streamName = "/nested-module:depth1-cont/depth2-leaf1";
assertMappedData(prepareMap(streamName, uri, outputType),
- ListenersBroker.streamEntry(streamName, "description", "location", Set.of(new EncodingName(outputType))));
+ AbstractRestconfStreamRegistry.streamEntry(streamName, "description", "location",
+ Set.of(new EncodingName(outputType))));
}
@Test
final var uri = "uri";
assertMappedData(prepareMap("notifi", uri, outputType),
- ListenersBroker.streamEntry("notifi", "description", "location", Set.of(new EncodingName(outputType))));
+ AbstractRestconfStreamRegistry.streamEntry("notifi", "description", "location",
+ Set.of(new EncodingName(outputType))));
}
private static Map<QName, Object> prepareMap(final String name, final String uri, final String outputType) {
return Map.of(
- ListenersBroker.NAME_QNAME, name,
- ListenersBroker.LOCATION_QNAME, uri,
- ListenersBroker.ENCODING_QNAME, outputType,
- ListenersBroker.DESCRIPTION_QNAME, "description");
+ AbstractRestconfStreamRegistry.NAME_QNAME, name,
+ AbstractRestconfStreamRegistry.LOCATION_QNAME, uri,
+ AbstractRestconfStreamRegistry.ENCODING_QNAME, outputType,
+ AbstractRestconfStreamRegistry.DESCRIPTION_QNAME, "description");
}
private static void assertMappedData(final Map<QName, Object> map, final MapEntryNode mappedData) {