--- /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.jaxrs;
+
+import java.text.ParseException;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.PathParam;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.api.ApiPath;
+
+/**
+ * A JAX-RS parsing bridge to {@link ApiPath}.
+ */
+@NonNullByDefault
+final class JaxRsApiPath {
+ final ApiPath apiPath;
+
+ /**
+ * Default constructor for {@link PathParam} integration.
+ *
+ * @param str Path parameter value
+ * @throws NullPointerException if {@code str} is {@code null}
+ * @throws BadRequestException if {@code str} cannmot be interpreted as an {@link ApiPath}
+ */
+ JaxRsApiPath(final String str) {
+ try {
+ apiPath = ApiPath.parseUrl(str);
+ } catch (ParseException e) {
+ throw new BadRequestException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return apiPath.toString();
+ }
+}
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
+import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.api.MediaTypes;
import org.opendaylight.restconf.common.errors.RestconfError;
import org.opendaylight.restconf.common.errors.RestconfFuture;
@DELETE
@Path("/data/{identifier:.+}")
@SuppressWarnings("checkstyle:abbreviationAsWordInName")
- public void dataDELETE(@Encoded @PathParam("identifier") final String identifier,
+ public void dataDELETE(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
@Suspended final AsyncResponse ar) {
- server.dataDELETE(identifier).addCallback(new JaxRsRestconfCallback<>(ar) {
+ server.dataDELETE(identifier.apiPath).addCallback(new JaxRsRestconfCallback<>(ar) {
@Override
Response transform(final Empty result) {
return Response.noContent().build();
MediaType.APPLICATION_XML,
MediaType.TEXT_XML
})
- public void dataGET(@Encoded @PathParam("identifier") final String identifier,
+ public void dataGET(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
final var readParams = QueryParams.newReadDataParams(uriInfo);
- completeDataGET(server.dataGET(identifier, readParams), readParams, ar);
+ completeDataGET(server.dataGET(identifier.apiPath, readParams), readParams, ar);
}
private static void completeDataGET(final RestconfFuture<NormalizedNodePayload> future,
MediaType.APPLICATION_XML,
MediaType.TEXT_XML
})
- public void dataXmlPATCH(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+ public void dataXmlPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
@Suspended final AsyncResponse ar) {
try (var xmlBody = new XmlResourceBody(body)) {
- completeDataPATCH(server.dataPATCH(identifier, xmlBody), ar);
+ completeDataPATCH(server.dataPATCH(identifier.apiPath, xmlBody), ar);
}
}
MediaTypes.APPLICATION_YANG_DATA_JSON,
MediaType.APPLICATION_JSON,
})
- public void dataJsonPATCH(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+ public void dataJsonPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
@Suspended final AsyncResponse ar) {
try (var jsonBody = new JsonResourceBody(body)) {
- completeDataPATCH(server.dataPATCH(identifier, jsonBody), ar);
+ completeDataPATCH(server.dataPATCH(identifier.apiPath, jsonBody), ar);
}
}
MediaTypes.APPLICATION_YANG_DATA_JSON,
MediaTypes.APPLICATION_YANG_DATA_XML
})
- public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final String identifier,
+ public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
final InputStream body, @Suspended final AsyncResponse ar) {
try (var jsonBody = new JsonPatchBody(body)) {
- completeDataYangPATCH(server.dataPATCH(identifier, jsonBody), ar);
+ completeDataYangPATCH(server.dataPATCH(identifier.apiPath, jsonBody), ar);
}
}
MediaTypes.APPLICATION_YANG_DATA_JSON,
MediaTypes.APPLICATION_YANG_DATA_XML
})
- public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
- @Suspended final AsyncResponse ar) {
+ public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
+ final InputStream body, @Suspended final AsyncResponse ar) {
try (var xmlBody = new XmlPatchBody(body)) {
- completeDataYangPATCH(server.dataPATCH(identifier, xmlBody), ar);
+ completeDataYangPATCH(server.dataPATCH(identifier.apiPath, xmlBody), ar);
}
}
MediaTypes.APPLICATION_YANG_DATA_JSON,
MediaType.APPLICATION_JSON,
})
- public void postDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+ public void postDataJSON(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
- completeDataPOST(server.dataPOST(identifier, new JsonDataPostBody(body), QueryParams.normalize(uriInfo)),
- uriInfo, ar);
+ completeDataPOST(server.dataPOST(identifier.apiPath, new JsonDataPostBody(body),
+ QueryParams.normalize(uriInfo)), uriInfo, ar);
}
/**
MediaType.APPLICATION_XML,
MediaType.TEXT_XML
})
- public void postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+ public void postDataXML(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
- completeDataPOST(server.dataPOST(identifier, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
+ completeDataPOST(server.dataPOST(identifier.apiPath, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
uriInfo, ar);
}
MediaTypes.APPLICATION_YANG_DATA_JSON,
MediaType.APPLICATION_JSON,
})
- public void dataJsonPUT(@Encoded @PathParam("identifier") final String identifier,
+ public void dataJsonPUT(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
try (var jsonBody = new JsonResourceBody(body)) {
- completeDataPUT(server.dataPUT(identifier, jsonBody, QueryParams.normalize(uriInfo)), ar);
+ completeDataPUT(server.dataPUT(identifier.apiPath, jsonBody, QueryParams.normalize(uriInfo)), ar);
}
}
MediaType.APPLICATION_XML,
MediaType.TEXT_XML
})
- public void dataXmlPUT(@Encoded @PathParam("identifier") final String identifier,
+ public void dataXmlPUT(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
try (var xmlBody = new XmlResourceBody(body)) {
- completeDataPUT(server.dataPUT(identifier, xmlBody, QueryParams.normalize(uriInfo)), ar);
+ completeDataPUT(server.dataPUT(identifier.apiPath, xmlBody, QueryParams.normalize(uriInfo)), ar);
}
}
@GET
@Path("/operations/{operation:.+}")
@Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
- public void operationsJsonGET(@PathParam("operation") final String operation, final AsyncResponse ar) {
- completeOperationsGet(server.operationsGET(operation), ar, OperationsGetResult::toJSON);
+ public void operationsJsonGET(@PathParam("operation") final JaxRsApiPath operation, final AsyncResponse ar) {
+ completeOperationsGet(server.operationsGET(operation.apiPath), ar, OperationsGetResult::toJSON);
}
private static void completeOperationsJsonGet(final RestconfFuture<OperationsGetResult> future,
@GET
@Path("/operations/{operation:.+}")
@Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
- public void operationsXmlGET(@PathParam("operation") final String operation, final AsyncResponse ar) {
- completeOperationsXmlGet(server.operationsGET(operation), ar);
+ public void operationsXmlGET(@PathParam("operation") final JaxRsApiPath operation, final AsyncResponse ar) {
+ completeOperationsXmlGet(server.operationsGET(operation.apiPath), ar);
}
private static void completeOperationsXmlGet(final RestconfFuture<OperationsGetResult> future,
MediaType.APPLICATION_XML,
MediaType.TEXT_XML
})
- public void operationsXmlPOST(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
- @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+ public void operationsXmlPOST(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
+ final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
try (var xmlBody = new XmlOperationInputBody(body)) {
- operationsPOST(identifier, uriInfo, ar, xmlBody);
+ operationsPOST(identifier.apiPath, uriInfo, ar, xmlBody);
}
}
MediaType.APPLICATION_XML,
MediaType.TEXT_XML
})
- public void operationsJsonPOST(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
- @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+ public void operationsJsonPOST(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
+ final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
try (var jsonBody = new JsonOperationInputBody(body)) {
- operationsPOST(identifier, uriInfo, ar, jsonBody);
+ operationsPOST(identifier.apiPath, uriInfo, ar, jsonBody);
}
}
- private void operationsPOST(final String identifier, final UriInfo uriInfo, final AsyncResponse ar,
+ private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
final OperationInputBody body) {
server.operationsPOST(uriInfo.getBaseUri(), identifier, body)
.addCallback(new JaxRsRestconfCallback<OperationOutput>(ar) {
import java.io.IOException;
import java.io.InputStream;
import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.patch.PatchContext;
import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
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;
+import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
targetUrl = IdentifierCodec.serialize(urlPath, context) + target;
}
- return ParserIdentifier.toInstanceIdentifier(targetUrl, context, null).getInstanceIdentifier();
+ try {
+ return ParserIdentifier.toInstanceIdentifier(targetUrl, context, null).getInstanceIdentifier();
+ } catch (RestconfDocumentedException e) {
+ throw new RestconfDocumentedException("Failed to parse target " + target,
+ ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE, e);
+ }
}
/**
/**
* Valid top level node name.
*
- * @param path path of node
+ * @param apiPath path of node
* @param data data
*/
@VisibleForTesting
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.dom.api.DOMMountPoint;
+import org.opendaylight.mdsal.dom.api.DOMMountPointService;
+import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.ApiPath.Step;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
this.mountPoint = mountPoint;
}
+ // FIXME: NETCONF-773: this recursion should really live in MdsalRestconfServer
+ public static @NonNull InstanceIdentifierContext ofApiPath(final ApiPath path,
+ final EffectiveModelContext modelContext, final DOMMountPointService mountPointService) {
+ final var steps = path.steps();
+ final var limit = steps.size() - 1;
+
+ var prefix = 0;
+ DOMMountPoint currentMountPoint = null;
+ var currentModelContext = modelContext;
+ while (prefix <= limit) {
+ final var mount = indexOfMount(steps, prefix, limit);
+ if (mount == -1) {
+ break;
+ }
+
+ final var mountService = currentMountPoint == null ? mountPointService
+ : currentMountPoint.getService(DOMMountPointService.class).orElse(null);
+ if (mountService == null) {
+ throw new RestconfDocumentedException("Mount point service is not available",
+ ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED);
+ }
+
+ final var mountPath = IdentifierCodec.deserialize(path.subPath(prefix, mount), modelContext);
+ final var userPath = path.subPath(0, mount);
+ final var nextMountPoint = mountService.getMountPoint(mountPath)
+ .orElseThrow(() -> new RestconfDocumentedException("Mount point '" + userPath + "' does not exist",
+ ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT));
+ final var nextModelContext = nextMountPoint.getService(DOMSchemaService.class)
+ .orElseThrow(() -> new RestconfDocumentedException(
+ "Mount point " + userPath + " does not expose DOMSchemaService",
+ ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT))
+ .getGlobalContext();
+
+ prefix = mount + 1;
+ currentModelContext = nextModelContext;
+ currentMountPoint = nextMountPoint;
+ }
+
+ final var result = YangInstanceIdentifierDeserializer.create(currentModelContext, path.subPath(prefix));
+ return InstanceIdentifierContext.ofPath(result.stack, result.node, result.path, currentMountPoint);
+ }
+
+ private static int indexOfMount(final ImmutableList<Step> steps, final int fromIndex, final int limit) {
+ for (int i = fromIndex; i <= limit; ++ i) {
+ final var step = steps.get(i);
+ if ("yang-ext".equals(step.module()) && "mount".equals(step.identifier().getLocalName())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
public static @NonNull InstanceIdentifierContext ofLocalRoot(final EffectiveModelContext context) {
return new Root(context, null);
}
*/
package org.opendaylight.restconf.nb.rfc8040.utils.parser;
+import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
return YangInstanceIdentifierSerializer.create(schemaContext, data);
}
- public static YangInstanceIdentifier deserialize(final String data, final EffectiveModelContext schemaContext) {
+ public static YangInstanceIdentifier deserialize(final ApiPath data, final EffectiveModelContext schemaContext) {
return data == null ? YangInstanceIdentifier.of()
: YangInstanceIdentifierDeserializer.create(schemaContext, data).path;
}
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
+import java.text.ParseException;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.Iterator;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
+import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
import org.opendaylight.restconf.nb.rfc8040.rests.services.api.SchemaExportContext;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.common.Revision;
import org.opendaylight.yangtools.yang.common.YangNames;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.model.api.Module;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
// nested path split on yang-ext:mount. This splitting needs to be based on consulting the
// EffectiveModelContext and allowing it only where yang-ext:mount is actually used in models.
private static final String MOUNT = "yang-ext:mount";
- private static final Splitter MP_SPLITTER = Splitter.on("/" + MOUNT);
private static final Splitter SLASH_SPLITTER = Splitter.on('/');
private ParserIdentifier() {
}
/**
- * Make {@link InstanceIdentifierContext} from {@link String} identifier
+ * Make {@link InstanceIdentifierContext} from {@link String} identifier.
* <br>
* For identifiers of data NOT behind mount points returned
* {@link InstanceIdentifierContext} is prepared with {@code null} reference of {@link DOMMountPoint} and with
* {@link InstanceIdentifierContext} is prepared with reference of {@link DOMMountPoint} and its
* own {@link SchemaContext}.
*
- * @param identifier
- * - path identifier
- * @param schemaContext
- * - controller schema context
- * @param mountPointService
- * - mount point service
+ * @param identifier path identifier
+ * @param schemaContext controller schema context
+ * @param mountPointService mount point service
* @return {@link InstanceIdentifierContext}
*/
- // FIXME: NETCONF-631: this method should not be here, it should be a static factory in InstanceIdentifierContext:
- //
- // @NonNull InstanceIdentifierContext forUrl(identifier, schemaContexxt, mountPointService)
- //
public static InstanceIdentifierContext toInstanceIdentifier(final String identifier,
final EffectiveModelContext schemaContext, final @Nullable DOMMountPointService mountPointService) {
- if (identifier == null || !identifier.contains(MOUNT)) {
- return createIIdContext(schemaContext, identifier, null);
- }
- if (mountPointService == null) {
- throw new RestconfDocumentedException("Mount point service is not available");
- }
-
- final Iterator<String> pathsIt = MP_SPLITTER.split(identifier).iterator();
- final String mountPointId = pathsIt.next();
- final YangInstanceIdentifier mountPath = IdentifierCodec.deserialize(mountPointId, schemaContext);
- final DOMMountPoint mountPoint = mountPointService.getMountPoint(mountPath)
- .orElseThrow(() -> new RestconfDocumentedException("Mount point does not exist.",
- ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT));
-
- final EffectiveModelContext mountSchemaContext = coerceModelContext(mountPoint);
- final String pathId = pathsIt.next().replaceFirst("/", "");
- return createIIdContext(mountSchemaContext, pathId, mountPoint);
- }
-
- /**
- * Method to create {@link InstanceIdentifierContext} from {@link YangInstanceIdentifier}
- * and {@link SchemaContext}, {@link DOMMountPoint}.
- *
- * @param url Invocation URL
- * @param schemaContext SchemaContext in which the path is to be interpreted in
- * @param mountPoint A mount point handle, if the URL is being interpreted relative to a mount point
- * @return {@link InstanceIdentifierContext}
- * @throws RestconfDocumentedException if the path cannot be resolved
- */
- private static InstanceIdentifierContext createIIdContext(final EffectiveModelContext schemaContext,
- final String url, final @Nullable DOMMountPoint mountPoint) {
- // First things first: an empty path means data invocation on SchemaContext
- if (url == null) {
- return mountPoint != null ? InstanceIdentifierContext.ofMountPointRoot(mountPoint, schemaContext)
- : InstanceIdentifierContext.ofLocalRoot(schemaContext);
+ final ApiPath apiPath;
+ try {
+ apiPath = ApiPath.parseUrl(identifier);
+ } catch (ParseException e) {
+ throw new RestconfDocumentedException(e.getMessage(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
}
- final var result = YangInstanceIdentifierDeserializer.create(schemaContext, url);
- return InstanceIdentifierContext.ofPath(result.stack, result.node, result.path, mountPoint);
+ return InstanceIdentifierContext.ofApiPath(apiPath, schemaContext, mountPointService);
}
/**
* @return A {@link RestconfFuture} of the operation
*/
@SuppressWarnings("checkstyle:abbreviationAsWordInName")
- RestconfFuture<Empty> dataDELETE(String identifier);
+ RestconfFuture<Empty> dataDELETE(ApiPath identifier);
/**
* Return the content of the datastore.
* @param readParams {@link ReadDataParams} for this request
* @return A {@link RestconfFuture} of the {@link NormalizedNodePayload} content
*/
- RestconfFuture<NormalizedNodePayload> dataGET(String identifier, ReadDataParams readParams);
+ RestconfFuture<NormalizedNodePayload> dataGET(ApiPath identifier, ReadDataParams readParams);
/**
* Partially modify the target data resource, as defined in
* @param body data node for put to config DS
* @return A {@link RestconfFuture} of the operation
*/
- RestconfFuture<Empty> dataPATCH(String identifier, ResourceBody body);
+ RestconfFuture<Empty> dataPATCH(ApiPath identifier, ResourceBody body);
/**
* Ordered list of edits that are applied to the datastore by the server, as defined in
* @param body YANG Patch body
* @return A {@link RestconfFuture} of the {@link PatchStatusContext} content
*/
- RestconfFuture<PatchStatusContext> dataPATCH(String identifier, PatchBody body);
+ RestconfFuture<PatchStatusContext> dataPATCH(ApiPath identifier, PatchBody body);
RestconfFuture<CreateResource> dataPOST(ChildBody body, Map<String, String> queryParameters);
- RestconfFuture<? extends DataPostResult> dataPOST(String identifier, DataPostBody body,
+ RestconfFuture<? extends DataPostResult> dataPOST(ApiPath identifier, DataPostBody body,
Map<String, String> queryParameters);
/**
* @param queryParameters Query parameters
* @return A {@link RestconfFuture} completing with {@link DataPutResult}
*/
- RestconfFuture<DataPutResult> dataPUT(String identifier, ResourceBody body, Map<String, String> queryParameters);
+ RestconfFuture<DataPutResult> dataPUT(ApiPath identifier, ResourceBody body, Map<String, String> queryParameters);
/**
- * Return the set of supported RPCs supported by {@link #operationsPOST(URI, String, OperationInputBody)},
+ * Return the set of supported RPCs supported by {@link #operationsPOST(URI, ApiPath, OperationInputBody)},
* as expressed in the <a href="https://www.rfc-editor.org/rfc/rfc8040#page-84">ietf-restconf.yang</a>
* {@code container operations} statement.
*
* @param operation An operation
* @return A {@link RestconfFuture} completing with an {@link OperationsGetResult}
*/
- RestconfFuture<OperationsGetResult> operationsGET(String operation);
+ RestconfFuture<OperationsGetResult> operationsGET(ApiPath operation);
/**
* Invoke an RPC operation, as defined in
// FIXME: 'operation' should really be an ApiIdentifier with non-null module, but we also support ang-ext:mount,
// and hence it is a path right now
// FIXME: use ApiPath instead of String
- RestconfFuture<OperationOutput> operationsPOST(URI restconfURI, String operation, OperationInputBody body);
+ RestconfFuture<OperationOutput> operationsPOST(URI restconfURI, ApiPath operation, OperationInputBody body);
/**
* Return the revision of {@code ietf-yang-library} module implemented by this server, as defined in
*/
package org.opendaylight.restconf.server.mdsal;
-import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMRpcService;
import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
+import org.opendaylight.restconf.api.ApiPath;
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.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
import org.opendaylight.restconf.server.api.DataPostResult;
import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
}
@Override
- public RestconfFuture<Empty> dataDELETE(final String identifier) {
+ public RestconfFuture<Empty> dataDELETE(final ApiPath identifier) {
final var reqPath = bindRequestPath(identifier);
final var strategy = getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
return strategy.delete(reqPath.getInstanceIdentifier());
}
@Override
- public RestconfFuture<NormalizedNodePayload> dataGET(final String identifier, final ReadDataParams readParams) {
+ public RestconfFuture<NormalizedNodePayload> dataGET(final ApiPath identifier, final ReadDataParams readParams) {
return readData(bindRequestPath(identifier), readParams);
}
}
@Override
- public RestconfFuture<Empty> dataPATCH(final String identifier, final ResourceBody body) {
+ public RestconfFuture<Empty> dataPATCH(final ApiPath identifier, final ResourceBody body) {
return dataPATCH(bindRequestPath(identifier), body);
}
}
@Override
- public RestconfFuture<PatchStatusContext> dataPATCH(final String identifier, final PatchBody body) {
+ public RestconfFuture<PatchStatusContext> dataPATCH(final ApiPath identifier, final PatchBody body) {
return dataPATCH(bindRequestPath(identifier), body);
}
}
@Override
- public RestconfFuture<? extends DataPostResult> dataPOST(final String identifier, final DataPostBody body,
+ public RestconfFuture<? extends DataPostResult> dataPOST(final ApiPath identifier, final DataPostBody body,
final Map<String, String> queryParameters) {
final var reqPath = bindRequestPath(identifier);
if (reqPath.getSchemaNode() instanceof ActionDefinition) {
}
@Override
- public RestconfFuture<DataPutResult> dataPUT(final String identifier, final ResourceBody body,
+ public RestconfFuture<DataPutResult> dataPUT(final ApiPath identifier, final ResourceBody body,
final Map<String, String> queryParameters) {
return dataPUT(bindRequestPath(identifier), body, queryParameters);
}
}
@Override
- public RestconfFuture<OperationsGetResult> operationsGET(final String operation) {
+ public RestconfFuture<OperationsGetResult> operationsGET(final ApiPath operation) {
// get current module RPCs/actions by RPC/action name
final var inference = bindRequestPath(operation).inference();
if (inference.isEmpty()) {
}
@Override
- public RestconfFuture<OperationOutput> operationsPOST(final URI restconfURI, final String apiPath,
+ public RestconfFuture<OperationOutput> operationsPOST(final URI restconfURI, final ApiPath apiPath,
final OperationInputBody body) {
final var currentContext = databindProvider.currentContext();
final var reqPath = bindRequestPath(currentContext, apiPath);
.map(Revision::toString).orElse(""))));
}
- private @NonNull InstanceIdentifierContext bindRequestPath(final String identifier) {
+ private @NonNull InstanceIdentifierContext bindRequestPath(final @NonNull ApiPath identifier) {
return bindRequestPath(databindProvider.currentContext(), identifier);
}
- private @NonNull InstanceIdentifierContext bindRequestPath(final DatabindContext databind,
- final String identifier) {
- // FIXME: go through ApiPath first. That part should eventually live in callers
+ private @NonNull InstanceIdentifierContext bindRequestPath(final @NonNull DatabindContext databind,
+ final @NonNull ApiPath identifier) {
// FIXME: DatabindContext looks like it should be internal
- return verifyNotNull(ParserIdentifier.toInstanceIdentifier(requireNonNull(identifier), databind.modelContext(),
- mountPointService));
+ return InstanceIdentifierContext.ofApiPath(identifier, databind.modelContext(), mountPointService);
}
private @NonNull InstanceIdentifierContext bindRequestRoot() {
@ExtendWith(MockitoExtension.class)
abstract class AbstractRestconfTest extends AbstractJukeboxTest {
+ static final JaxRsApiPath JUKEBOX_API_PATH = new JaxRsApiPath("example-jukebox:jukebox");
+
@Mock
UriInfo uriInfo;
@Mock
() -> DatabindContext.ofModel(IID_SCHEMA), dataBroker, rpcService, actionService, mountPointService));
doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
doReturn(true).when(asyncResponse).resume(captor.capture());
- restconf.postDataJSON("instance-identifier-module:cont/cont1/reset",
+ restconf.postDataJSON(new JaxRsApiPath("instance-identifier-module:cont/cont1/reset"),
stringInputStream("""
{
"instance-identifier-module:input": {
@Test
void testOperationsContentByIdentifier() {
+ final var apiPath = new JaxRsApiPath("foo:new1");
assertEquals("""
- { "foo:new1" : [null] }""", assertEntity(200, ar -> restconf.operationsJsonGET("foo:new1", ar)));
+ { "foo:new1" : [null] }""", assertEntity(200, ar -> restconf.operationsJsonGET(apiPath, ar)));
assertEquals("""
- <new1 xmlns="foo"/>""", assertEntity(200, ar -> restconf.operationsXmlGET("foo:new1", ar)));
+ <new1 xmlns="foo"/>""", assertEntity(200, ar -> restconf.operationsXmlGET(apiPath, ar)));
}
}
.when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
- assertNull(assertEntity(204, ar -> restconf.dataDELETE("example-jukebox:jukebox", ar)));
+ assertNull(assertEntity(204, ar -> restconf.dataDELETE(JUKEBOX_API_PATH, ar)));
}
@Test
.when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
doReturn(true).when(tx).cancel();
- final var error = assertError(ar -> restconf.dataDELETE("example-jukebox:jukebox", ar));
+ final var error = assertError(ar -> restconf.dataDELETE(JUKEBOX_API_PATH, ar));
assertEquals(ErrorType.PROTOCOL, error.getErrorType());
assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
}
.when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
- assertNull(assertEntity(204,
- ar -> restconf.dataDELETE("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", ar)));
+ assertNull(assertEntity(204, ar -> restconf.dataDELETE(
+ new JaxRsApiPath("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox"), ar)));
}
}
doReturn(immediateFluentFuture(Optional.empty()))
.when(tx).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
- assertEquals(EMPTY_JUKEBOX,
- assertNormalizedNode(200, ar -> restconf.dataGET("example-jukebox:jukebox", uriInfo, ar)));
+ assertEquals(EMPTY_JUKEBOX, assertNormalizedNode(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar)));
}
@Test
// response must contain all child nodes from config and operational containers merged in one container
final var data = assertInstanceOf(ContainerNode.class,
- assertNormalizedNode(200, ar ->
- restconf.dataGET("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", uriInfo, ar)));
+ assertNormalizedNode(200, ar -> restconf.dataGET(
+ new JaxRsApiPath("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox"), uriInfo, ar)));
assertEquals(3, data.size());
assertNotNull(data.childByArg(CONT_PLAYER.name()));
assertNotNull(data.childByArg(LIBRARY_NID));
doReturn(immediateFluentFuture(Optional.empty()))
.when(tx).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
- final var error = assertError(ar -> restconf.dataGET("example-jukebox:jukebox", uriInfo, ar));
+ final var error = assertError(ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar));
assertEquals(ErrorType.PROTOCOL, error.getErrorType());
assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
assertEquals("Request could not be completed because the relevant data model content does not exist",
// response must contain only config data
final var data = assertInstanceOf(ContainerNode.class,
- assertNormalizedNode(200, ar -> restconf.dataGET("example-jukebox:jukebox", uriInfo, ar)));
+ assertNormalizedNode(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar)));
// config data present
assertNotNull(data.childByArg(CONT_PLAYER.name()));
assertNotNull(data.childByArg(LIBRARY_NID));
.read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
// response must contain only operational data
- final var data = assertInstanceOf(ContainerNode.class,
- assertNormalizedNode(200, ar -> restconf.dataGET("example-jukebox:jukebox", uriInfo, ar)));
+ final var data = assertInstanceOf(ContainerNode.class, assertNormalizedNode(200,
+ ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar)));
// state data present
assertNotNull(data.childByArg(CONT_PLAYER.name()));
assertNotNull(data.childByArg(PLAYLIST_NID));
doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=name%20of%20band"),
- assertResponse(201, ar -> restconf.postDataJSON("example-jukebox:jukebox", stringInputStream("""
+ assertResponse(201, ar -> restconf.postDataJSON(JUKEBOX_API_PATH, stringInputStream("""
{
"example-jukebox:playlist" : {
"name" : "name of band",
doReturn(immediateTrueFluentFuture()).when(readTx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
doNothing().when(rwTx).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
- assertNull(assertEntity(204, ar -> restconf.dataJsonPUT("example-jukebox:jukebox", uriInfo,
- stringInputStream("""
- {
- "example-jukebox:jukebox" : {
- "player": {
- "gap": "0.2"
- }
- }
- }"""), ar)));
+ assertNull(assertEntity(204, ar -> restconf.dataJsonPUT(JUKEBOX_API_PATH, uriInfo, stringInputStream("""
+ {
+ "example-jukebox:jukebox" : {
+ "player": {
+ "gap": "0.2"
+ }
+ }
+ }"""), ar)));
}
@Test
doReturn(Optional.empty()).when(mountPoint).getService(NetconfDataTreeService.class);
assertNull(assertEntity(204, ar -> restconf.dataXmlPUT(
- "example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", uriInfo, stringInputStream("""
- <jukebox xmlns="http://example.com/ns/example-jukebox">
- <player>
- <gap>0.2</gap>
- </player>
- </jukebox>"""), ar)));
+ new JaxRsApiPath("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox"), uriInfo,
+ stringInputStream("""
+ <jukebox xmlns="http://example.com/ns/example-jukebox">
+ <player>
+ <gap>0.2</gap>
+ </player>
+ </jukebox>"""), ar)));
}
@Test
.exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
assertNull(assertEntity(201, ar -> restconf.dataJsonPUT(
- "example-jukebox:jukebox/playlist=0/song=3", uriInfo, stringInputStream("""
+ new JaxRsApiPath("example-jukebox:jukebox/playlist=0/song=3"), uriInfo, stringInputStream("""
{
"example-jukebox:song" : [
{
.read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
assertNull(assertEntity(201, ar -> restconf.dataJsonPUT(
- "example-jukebox:jukebox/playlist=0/song=3", uriInfo, stringInputStream("""
+ new JaxRsApiPath("example-jukebox:jukebox/playlist=0/song=3"), uriInfo, stringInputStream("""
{
"example-jukebox:song" : [
{
.read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
assertNull(assertEntity(201, ar -> restconf.dataJsonPUT(
- "example-jukebox:jukebox/playlist=0/song=3", uriInfo, stringInputStream("""
+ new JaxRsApiPath("example-jukebox:jukebox/playlist=0/song=3"), uriInfo, stringInputStream("""
{
"example-jukebox:song" : [
{
.read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
assertNull(assertEntity(201, ar -> restconf.dataJsonPUT(
- "example-jukebox:jukebox/playlist=0/song=3", uriInfo, stringInputStream("""
+ new JaxRsApiPath("example-jukebox:jukebox/playlist=0/song=3"), uriInfo, stringInputStream("""
{
"example-jukebox:song" : [
{
@ExtendWith(MockitoExtension.class)
class RestconfOperationsGetTest extends AbstractRestconfTest {
- private static final String DEVICE_ID = "network-topology:network-topology/topology=topology-netconf/"
- + "node=device/yang-ext:mount";
- private static final String DEVICE_RPC1_MODULE1_ID = DEVICE_ID + "module1:dummy-rpc1-module1";
+ private static final JaxRsApiPath DEVICE_ID =
+ new JaxRsApiPath("network-topology:network-topology/topology=topology-netconf/node=device/yang-ext:mount");
+ private static final JaxRsApiPath DEVICE_RPC1_MODULE1_ID = new JaxRsApiPath("network-topology:network-topology/"
+ + "topology=topology-netconf/node=device/yang-ext:mount/module1:dummy-rpc1-module1");
private static final String EXPECTED_JSON = """
{
"ietf-restconf:operations" : {
doReturn(false).when(result).isEmpty();
prepNNC(result);
- assertSame(result, assertNormalizedNode(200, ar -> restconf.operationsXmlPOST("invoke-rpc-module:rpc-test",
- stringInputStream("""
+ assertSame(result, assertNormalizedNode(200, ar -> restconf.operationsXmlPOST(
+ new JaxRsApiPath("invoke-rpc-module:rpc-test"), stringInputStream("""
<input xmlns="invoke:rpc:module"/>"""), uriInfo, ar)));
}
doReturn(true).when(result).isEmpty();
prepNNC(result);
- assertNull(assertEntity(204, ar -> restconf.operationsJsonPOST("invoke-rpc-module:rpc-test",
+ assertNull(assertEntity(204, ar -> restconf.operationsJsonPOST(new JaxRsApiPath("invoke-rpc-module:rpc-test"),
stringInputStream("""
{
"invoke-rpc-module:input" : {
doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of()))).when(rpcService)
.invokeRpc(RPC, INPUT);
- assertEquals(OUTPUT, assertNormalizedNode(200, ar -> restconf.operationsXmlPOST("invoke-rpc-module:rpc-test",
- stringInputStream("""
+ assertEquals(OUTPUT, assertNormalizedNode(200, ar -> restconf.operationsXmlPOST(
+ new JaxRsApiPath("invoke-rpc-module:rpc-test"), stringInputStream("""
<input xmlns="invoke:rpc:module">
<cont>
<lf>test</lf>
"No implementation of RPC " + RPC + " available.");
doReturn(Futures.immediateFailedFuture(exception)).when(rpcService).invokeRpc(RPC, INPUT);
- final var error = assertError(ar -> restconf.operationsJsonPOST("invoke-rpc-module:rpc-test",
+ final var error = assertError(ar -> restconf.operationsJsonPOST(new JaxRsApiPath("invoke-rpc-module:rpc-test"),
stringInputStream("""
{
"invoke-rpc-module:input" : {
assertEquals(OUTPUT, assertNormalizedNode(200,
ar -> restconf.operationsJsonPOST(
- "ietf-yang-library:modules-state/yang-ext:mount/invoke-rpc-module:rpc-test",
+ new JaxRsApiPath("ietf-yang-library:modules-state/yang-ext:mount/invoke-rpc-module:rpc-test"),
stringInputStream("""
{
"invoke-rpc-module:input" : {
final var error = assertError(
ar -> restconf.operationsJsonPOST(
- "ietf-yang-library:modules-state/yang-ext:mount/invoke-rpc-module:rpc-test",
+ new JaxRsApiPath("ietf-yang-library:modules-state/yang-ext:mount/invoke-rpc-module:rpc-test"),
stringInputStream("""
{
"invoke-rpc-module:input" : {
doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of())))
.when(rpcService).invokeRpc(RPC, INPUT);
- assertEquals(OUTPUT, assertNormalizedNode(200, ar -> restconf.operationsJsonPOST("invoke-rpc-module:rpc-test",
+ assertEquals(OUTPUT, assertNormalizedNode(200, ar -> restconf.operationsJsonPOST(
+ new JaxRsApiPath("invoke-rpc-module:rpc-test"),
stringInputStream("""
{
"invoke-rpc-module:input" : {
assertNotNull(patchContext.entities());
}
- final @NonNull PatchContext parse(final String uriPath, final String patchBody) throws IOException {
+ final @NonNull PatchContext parse(final String prefix, final String suffix, final String patchBody)
+ throws IOException {
+ final String uriPath;
+ if (prefix.isEmpty()) {
+ uriPath = suffix;
+ } else if (suffix.isEmpty()) {
+ uriPath = prefix;
+ } else {
+ uriPath = prefix + '/' + suffix;
+ }
+
final var iid = ParserIdentifier.toInstanceIdentifier(uriPath, IID_SCHEMA, mountPointService);
try (var body = bodyConstructor.apply(stringInputStream(patchBody))) {
public class JsonPatchBodyMountPointTest extends JsonPatchBodyTest {
@Override
String mountPrefix() {
- return "instance-identifier-module:cont/yang-ext:mount/";
+ return "instance-identifier-module:cont/yang-ext:mount";
}
@Override
@Test
public final void modulePatchDataTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
*/
@Test
public final void modulePatchCreateAndDeleteTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
@Test
public final void modulePatchValueMissingNegativeTest() throws Exception {
final var ex = assertThrows(RestconfDocumentedException.class,
- () -> parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ () -> parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
@Test
public final void modulePatchValueNotSupportedNegativeTest() throws Exception {
final var ex = assertThrows(RestconfDocumentedException.class,
- () -> parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ () -> parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
*/
@Test
public final void modulePatchCompleteTargetInURITest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
*/
@Test
public final void modulePatchMergeOperationOnListTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "Test merge operation",
*/
@Test
public final void modulePatchMergeOperationOnContainerTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "Test merge operation",
*/
@Test
public final void modulePatchSimpleLeafValueTest() throws Exception {
- final var returnValue = parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ final var returnValue = parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
*/
@Test
public final void modulePatchTargetTopLevelContainerWithEmptyURITest() throws Exception {
- checkPatchContext(parse(mountPrefix(), """
+ checkPatchContext(parse(mountPrefix(), "", """
{
"ietf-yang-patch:yang-patch" : {
"patch-id" : "test-patch",
*/
@Test
public final void modulePatchTargetTopLevelContainerWithFullPathURITest() throws Exception {
- final var returnValue = parse(mountPrefix() + "instance-identifier-patch-module:patch-cont", """
+ final var returnValue = parse(mountPrefix(), "instance-identifier-patch-module:patch-cont", """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "test-patch",
*/
@Test
public final void modulePatchTargetSecondLevelListWithFullPathURITest() throws Exception {
- final var returnValue = parse(
- mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=my-leaf-set", """
+ final var returnValue = parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=my-leaf-set",
+ """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "test-patch",
*/
@Test
public final void modulePatchTargetTopLevelAugmentedContainerTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "test-patch",
*/
@Test
public final void modulePatchTargetMapNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "map-patch",
*/
@Test
public final void modulePatchTargetLeafSetNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "set-patch",
*/
@Test
public final void modulePatchTargetUnkeyedListNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "list-patch",
*/
@Test
public final void modulePatchTargetCaseNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
{
"ietf-yang-patch:yang-patch": {
"patch-id": "choice-patch",
public class XmlPatchBodyMountPointTest extends XmlPatchBodyTest {
@Override
String mountPrefix() {
- return "instance-identifier-module:cont/yang-ext:mount/";
+ return "instance-identifier-module:cont/yang-ext:mount";
}
@Override
@Test
public final void moduleDataTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>this is test patch</comment>
@Test
public final void moduleDataValueMissingNegativeTest() throws Exception {
final var ex = assertThrows(RestconfDocumentedException.class,
- () -> parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ () -> parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>Test patch with missing value node for create operation</comment>
@Test
public final void moduleDataNotValueNotSupportedNegativeTest() throws Exception {
final var ex = assertThrows(RestconfDocumentedException.class,
- () -> parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ () -> parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>Test patch with not allowed value node for delete operation</comment>
*/
@Test
public final void moduleDataAbsoluteTargetPathTest() throws Exception {
- checkPatchContext(parse(mountPrefix(), """
+ checkPatchContext(parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>Test patch with absolute target path</comment>
*/
@Test
public final void modulePatchCompleteTargetInURITest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>Test to create and replace data in container directly using / sign as a target</comment>
*/
@Test
public final void moduleDataMergeOperationOnListTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>Test merge operation</patch-id>
<comment>This is test patch for merge operation on list</comment>
*/
@Test
public final void moduleDataMergeOperationOnContainerTest() throws Exception {
- checkPatchContext(parse(mountPrefix() + "instance-identifier-patch-module:patch-cont", """
+ checkPatchContext(parse(mountPrefix(), "instance-identifier-patch-module:patch-cont", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>Test merge operation</patch-id>
<comment>This is test patch for merge operation on container</comment>
*/
@Test
public final void modulePatchTargetTopLevelContainerWithEmptyURITest() throws Exception {
- checkPatchContext(parse(mountPrefix(), """
+ checkPatchContext(parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>Test patch applied to the top-level container with empty URI</comment>
*/
@Test
public final void modulePatchTargetTopLevelContainerWithFullPathURITest() throws Exception {
- final var returnValue = parse(mountPrefix() + "instance-identifier-patch-module:patch-cont", """
+ final var returnValue = parse(mountPrefix(), "instance-identifier-patch-module:patch-cont", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>Test patch applied to the top-level container with '/' in target</comment>
*/
@Test
public final void modulePatchTargetSecondLevelListWithFullPathURITest() throws Exception {
- final var returnValue = parse(
- mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=my-leaf-set",
+ final var returnValue = parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=my-leaf-set",
"""
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
*/
@Test
public final void moduleTargetTopLevelAugmentedContainerTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>This test patch for augmented element</comment>
*/
@Test
public final void moduleTargetMapNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>map-patch</patch-id>
<comment>YANG patch comment</comment>
*/
@Test
public final void modulePatchTargetLeafSetNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>set-patch</patch-id>
<comment>YANG patch comment</comment>
*/
@Test
public final void moduleTargetUnkeyedListNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>list-patch</patch-id>
<comment>YANG patch comment</comment>
*/
@Test
public final void moduleTargetCaseNodeTest() throws Exception {
- final var returnValue = parse(mountPrefix(), """
+ final var returnValue = parse(mountPrefix(), "", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>choice-patch</patch-id>
<comment>YANG patch comment</comment>
*/
@Test
public final void modulePatchSimpleLeafValueTest() throws Exception {
- final var returnValue = parse(mountPrefix() + "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
+ final var returnValue = parse(mountPrefix(), "instance-identifier-patch-module:patch-cont/my-list1=leaf1", """
<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
<patch-id>test-patch</patch-id>
<comment>this is test patch</comment>
import org.opendaylight.mdsal.dom.broker.DOMMountPointServiceImpl;
import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfSchemaService;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
when(mockSchemaService.getGlobalContext()).thenReturn(SCHEMA_CONTEXT_WITH_MOUNT_POINTS);
// make test - call service on mount point with null schema context
- assertThrows(IllegalStateException.class,
+ final var errors = assertThrows(RestconfDocumentedException.class,
// NULL_MOUNT_POINT contains null schema context
- () -> schemaService.getSchema(NULL_MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT));
+ () -> schemaService.getSchema(NULL_MOUNT_POINT + TEST_MODULE_BEHIND_MOUNT_POINT))
+ .getErrors();
+ assertEquals(1, errors.size());
+ final var error = errors.get(0);
+ assertEquals("Mount point mount-point-2:cont does not expose DOMSchemaService", error.getErrorMessage());
+ assertEquals(ErrorType.PROTOCOL, error.getErrorType());
+ assertEquals(ErrorTags.RESOURCE_DENIED_TRANSPORT, error.getErrorTag());
}
/**
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
+import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
* URI contains list identifier and leaf identifier.
*/
@Test
- void codecListAndLeafTest() {
- final var dataYangII = IdentifierCodec.deserialize(
- "list-test:top/list1=%2C%27\"%3A\"%20%2F,,foo/list2=a,b/result", SCHEMA_CONTEXT);
+ void codecListAndLeafTest() throws Exception {
+ final var dataYangII = IdentifierCodec.deserialize(ApiPath.parse(
+ "list-test:top/list1=%2C%27\"%3A\"%20%2F,,foo/list2=a,b/result"), SCHEMA_CONTEXT);
assertEquals("list-test:top/list1=%2C%27\"%3A\" %2F,,foo/list2=a,b/result",
IdentifierCodec.serialize(dataYangII, SCHEMA_CONTEXT));
}
* URI contains leaf list identifier.
*/
@Test
- void codecLeafListTest() {
+ void codecLeafListTest() throws Exception {
final var str = "list-test:top/Y=4";
- final var dataYangII = IdentifierCodec.deserialize(str, SCHEMA_CONTEXT);
+ final var dataYangII = IdentifierCodec.deserialize(ApiPath.parse(str), SCHEMA_CONTEXT);
assertEquals(str, IdentifierCodec.serialize(dataYangII, SCHEMA_CONTEXT));
}
}
/**
- * Positive test of serialization <code>YangInstanceIdentifier.EMPTY</code> and deserialization of result back to
- * <code>YangInstanceIdentifier.EMPTY</code>.
+ * Positive test of serialization {@link YangInstanceIdentifier#EMPTY} and deserialization of result back to
+ * {@link YangInstanceIdentifier#EMPTY}.
*/
@Test
- void codecDeserializeAndSerializeEmptyTest() {
+ void codecDeserializeAndSerializeEmptyTest() throws Exception {
final var serialized = IdentifierCodec.serialize(YangInstanceIdentifier.of(), SCHEMA_CONTEXT);
- assertEquals(YangInstanceIdentifier.of(), IdentifierCodec.deserialize(serialized, SCHEMA_CONTEXT));
+ assertEquals(YangInstanceIdentifier.of(), IdentifierCodec.deserialize(ApiPath.parse(serialized),
+ SCHEMA_CONTEXT));
}
}
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.when;
-import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
import org.opendaylight.mdsal.dom.broker.DOMMountPointServiceImpl;
import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
+import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.yangtools.yang.common.ErrorTag;
YangParserTestUtils.parseYangResourceDirectory("/parser-identifier");
// mount point and mount point service
+ private final DOMMountPointService mountPointService = new DOMMountPointServiceImpl();
private DOMMountPoint mountPoint;
- private DOMMountPointService mountPointService;
// mock mount point and mount point service
@Mock
@Before
public void setup() throws Exception {
- mountPointService = new DOMMountPointServiceImpl();
-
// create and register mount point
final var mountPointId = YangInstanceIdentifier.builder()
.node(QName.create("mount:point", "2016-06-02", "mount-container"))
.addService(DOMSchemaService.class, FixedDOMSchemaService.of(MODEL_CONTEXT_ON_MOUNT_POINT))
.register()
.getInstance();
-
- // register mount point with null schema context
- when(mockMountPointService.getMountPoint(YangInstanceIdentifier.of()))
- .thenReturn(Optional.of(mockMountPoint));
}
/**
assertEquals(MODEL_CONTEXT_ON_MOUNT_POINT, context.getSchemaContext());
}
- /**
- * Test of creating <code>InstanceIdentifierContext</code> when identifier is <code>null</code>.
- * <code>{@link YangInstanceIdentifier#empty()}</code> should be returned.
- */
- @Test
- public void toInstanceIdentifierNullIdentifierTest() {
- final var context = ParserIdentifier.toInstanceIdentifier(null, MODEL_CONTEXT, null);
- assertEquals(YangInstanceIdentifier.of(), context.getInstanceIdentifier());
- }
-
/**
* Negative test of creating <code>InstanceIdentifierContext</code> when <code>SchemaContext</code> is
* <code>null</code>. Test fails expecting <code>NullPointerException</code>.
@Test
public void toInstanceIdentifierMissingMountPointNegativeTest() {
final var ex = assertThrows(RestconfDocumentedException.class,
- () -> ParserIdentifier.toInstanceIdentifier("/yang-ext:mount", MODEL_CONTEXT, mountPointService));
+ () -> ParserIdentifier.toInstanceIdentifier("yang-ext:mount", MODEL_CONTEXT, mountPointService));
final var errors = ex.getErrors();
assertEquals(1, errors.size());
final var error = errors.get(0);
- assertEquals("Mount point does not exist.", error.getErrorMessage());
+ assertEquals("Mount point '' does not exist", error.getErrorMessage());
assertEquals(ErrorType.PROTOCOL, error.getErrorType());
assertEquals(ErrorTags.RESOURCE_DENIED_TRANSPORT, error.getErrorTag());
}
@Test
public void toSchemaExportContextFromIdentifierNullSchemaContextBehindMountPointNegativeTest() {
- assertThrows(IllegalStateException.class, () -> ParserIdentifier.toSchemaExportContextFromIdentifier(
- MODEL_CONTEXT, "/yang-ext:mount/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION,
- mockMountPointService, sourceProvider));
+ final var ex = assertThrows(RestconfDocumentedException.class,
+ () -> ParserIdentifier.toSchemaExportContextFromIdentifier(MODEL_CONTEXT,
+ "/yang-ext:mount/" + TEST_MODULE_NAME + "/" + TEST_MODULE_REVISION, mockMountPointService,
+ sourceProvider));
+ final var errors = ex.getErrors();
+ assertEquals(1, errors.size());
+ final var error = errors.get(0);
+ // FIXME: this should be something different
+ assertEquals("Identifier may not be empty", error.getErrorMessage());
+ assertEquals(ErrorType.PROTOCOL, error.getErrorType());
+ assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
}
/**
- * Test invoke RPC.
- *
- * <p>
- * Verify if RPC schema node was found.
+ * Test invoke RPC. Verify if RPC schema node was found.
*/
@Test
- public void invokeRpcTest() {
+ public void invokeRpcTest() throws Exception {
final var result = ParserIdentifier.toInstanceIdentifier(INVOKE_RPC, MODEL_CONTEXT, null);
// RPC schema node
assertEquals("rpc-test", rpcQName.getLocalName());
// other fields
- assertEquals(IdentifierCodec.deserialize(INVOKE_RPC, MODEL_CONTEXT), result.getInstanceIdentifier());
+ assertEquals(IdentifierCodec.deserialize(ApiPath.parse(INVOKE_RPC), MODEL_CONTEXT),
+ result.getInstanceIdentifier());
assertEquals(null, result.getMountPoint());
assertEquals(MODEL_CONTEXT, result.getSchemaContext());
}
/**
- * Test invoke RPC on mount point.
- *
- * <p>
- * Verify if RPC schema node was found.
+ * Test invoke RPC on mount point. Verify if RPC schema node was found.
*/
@Test
- public void invokeRpcOnMountPointTest() {
+ public void invokeRpcOnMountPointTest() throws Exception {
final var result = ParserIdentifier.toInstanceIdentifier(MOUNT_POINT_IDENT + "/" + INVOKE_RPC, MODEL_CONTEXT,
mountPointService);
// RPC schema node
- final QName rpcQName = result.getSchemaNode().getQName();
+ final var rpcQName = result.getSchemaNode().getQName();
assertEquals("invoke:rpc:module", rpcQName.getModule().getNamespace().toString());
assertEquals("rpc-test", rpcQName.getLocalName());
// other fields
- assertEquals(IdentifierCodec.deserialize(INVOKE_RPC, MODEL_CONTEXT), result.getInstanceIdentifier());
+ assertEquals(IdentifierCodec.deserialize(ApiPath.parse(INVOKE_RPC), MODEL_CONTEXT),
+ result.getInstanceIdentifier());
assertEquals(mountPoint, result.getMountPoint());
assertEquals(MODEL_CONTEXT_ON_MOUNT_POINT, result.getSchemaContext());
}
/**
- * Test Action.
- * Verify if Action schema node was found.
+ * Test Action. Verify if Action schema node was found.
*/
@Test
- public void invokeActionTest() {
+ public void invokeActionTest() throws Exception {
final var result = ParserIdentifier.toInstanceIdentifier(INVOKE_ACTION, MODEL_CONTEXT, null);
// Action schema node
- final QName actionQName = result.getSchemaNode().getQName();
+ final var actionQName = result.getSchemaNode().getQName();
assertEquals("https://example.com/ns/example-actions", actionQName.getModule().getNamespace().toString());
assertEquals("reset", actionQName.getLocalName());
// other fields
- assertEquals(IdentifierCodec.deserialize(INVOKE_ACTION, MODEL_CONTEXT), result.getInstanceIdentifier());
+ assertEquals(IdentifierCodec.deserialize(ApiPath.parse(INVOKE_ACTION), MODEL_CONTEXT),
+ result.getInstanceIdentifier());
assertNull(result.getMountPoint());
assertSame(MODEL_CONTEXT, result.getSchemaContext());
}
/**
- * Test invoke Action on mount point.
- * Verify if Action schema node was found.
+ * Test invoke Action on mount point. Verify if Action schema node was found.
*/
@Test
- public void invokeActionOnMountPointTest() {
+ public void invokeActionOnMountPointTest() throws Exception {
final var result = ParserIdentifier.toInstanceIdentifier(MOUNT_POINT_IDENT + "/" + INVOKE_ACTION,
MODEL_CONTEXT, mountPointService);
assertEquals("reset", actionQName.getLocalName());
// other fields
- assertEquals(IdentifierCodec.deserialize(INVOKE_ACTION, MODEL_CONTEXT), result.getInstanceIdentifier());
+ assertEquals(IdentifierCodec.deserialize(ApiPath.parse(INVOKE_ACTION), MODEL_CONTEXT),
+ result.getInstanceIdentifier());
assertEquals(mountPoint, result.getMountPoint());
assertEquals(MODEL_CONTEXT_ON_MOUNT_POINT, result.getSchemaContext());
}