diff options
Diffstat (limited to 'netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest')
25 files changed, 3653 insertions, 0 deletions
diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/Draft02.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/Draft02.java new file mode 100644 index 0000000..67c8729 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/Draft02.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.api; + +import org.opendaylight.yangtools.yang.common.QName; + +/** + * Base Draft for Restconf project. + * + * @deprecated Do not use old implementation of restconf draft. It will be replaced by Rfc8040. + */ +@Deprecated +public class Draft02 { + public interface MediaTypes { + String API = "application/yang.api"; + String DATASTORE = "application/yang.datastore"; + String DATA = "application/yang.data"; + String OPERATION = "application/yang.operation"; + String PATCH = "application/yang.patch"; + String PATCH_STATUS = "application/yang.patch-status"; + String STREAM = "application/yang.stream"; + } + + public interface RestConfModule { + String REVISION = "2013-10-19"; + + String NAME = "ietf-restconf"; + + String NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-restconf"; + + String RESTCONF_GROUPING_SCHEMA_NODE = "restconf"; + + String RESTCONF_CONTAINER_SCHEMA_NODE = "restconf"; + + String MODULES_CONTAINER_SCHEMA_NODE = "modules"; + + String MODULE_LIST_SCHEMA_NODE = "module"; + + String STREAMS_CONTAINER_SCHEMA_NODE = "streams"; + + String STREAM_LIST_SCHEMA_NODE = "stream"; + + String ERROR_LIST_SCHEMA_NODE = "error"; + + QName IETF_RESTCONF_QNAME = QName.create(Draft02.RestConfModule.NAMESPACE, Draft02.RestConfModule.REVISION, + Draft02.RestConfModule.NAME); + + QName ERRORS_QNAME = QName.create(IETF_RESTCONF_QNAME, "errors"); + + QName ERROR_LIST_QNAME = QName.create(IETF_RESTCONF_QNAME, ERROR_LIST_SCHEMA_NODE); + + QName ERROR_TYPE_QNAME = QName.create(IETF_RESTCONF_QNAME, "error-type"); + + QName ERROR_TAG_QNAME = QName.create(IETF_RESTCONF_QNAME, "error-tag"); + + QName ERROR_APP_TAG_QNAME = QName.create(IETF_RESTCONF_QNAME, "error-app-tag"); + + QName ERROR_MESSAGE_QNAME = QName.create(IETF_RESTCONF_QNAME, "error-message"); + + QName ERROR_INFO_QNAME = QName.create(IETF_RESTCONF_QNAME, "error-info"); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestConnector.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestConnector.java new file mode 100644 index 0000000..3200958 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestConnector.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.api; + +/* + * This is a simple dummy interface to allow us to create instances of RestconfProvider + * via the config subsystem. + */ +public interface RestConnector { + +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfConstants.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfConstants.java new file mode 100644 index 0000000..6a4ba93 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfConstants.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.api; + +public interface RestconfConstants { + + String IDENTIFIER = "identifier"; + +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfNormalizedNodeWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfNormalizedNodeWriter.java new file mode 100644 index 0000000..eb160bd --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfNormalizedNodeWriter.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.api; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +public interface RestconfNormalizedNodeWriter extends Flushable, Closeable { + + RestconfNormalizedNodeWriter write(NormalizedNode node) throws IOException; +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java new file mode 100644 index 0000000..0d0d2ab --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java @@ -0,0 +1,435 @@ +/* + * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.api; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.Encoded; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.netconf.sal.rest.api.Draft02.MediaTypes; +import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeContext; +import org.opendaylight.restconf.common.patch.Patch; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchStatusContext; + +/** + * The URI hierarchy for the RESTCONF resources consists of an entry point + * container, 4 top-level resources, and 1 field. + * <ul> + * <li><b>/restconf</b> - {@link #getRoot()} + * <ul> + * <li><b>/config</b> - {@link #readConfigurationData(String, UriInfo)} + * {@link #updateConfigurationData(String, NormalizedNodeContext, UriInfo)} + * {@link #createConfigurationData(NormalizedNodeContext, UriInfo)} + * {@link #createConfigurationData(String, NormalizedNodeContext, UriInfo)} + * {@link #deleteConfigurationData(String)} + * <li><b>/operational</b> - {@link #readOperationalData(String, UriInfo)} + * <li>/modules - {@link #getModules(UriInfo)} + * <ul> + * <li>/module + * </ul> + * <li><b>/operations</b> - + * {@link #invokeRpc(String, NormalizedNodeContext, UriInfo)} + * {@link #invokeRpc(String, NormalizedNodeContext, UriInfo)} + * <li>/version (field) + * </ul> + * </ul> + */ +@Path("/") +public interface RestconfService { + + String XML = "+xml"; + String JSON = "+json"; + + @GET + Object getRoot(); + + /** + * Get all modules supported by controller. + * + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#readData(UriInfo) + */ + @Deprecated + @GET + @Path("/modules") + @Produces({ + Draft02.MediaTypes.API + JSON, + Draft02.MediaTypes.API + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext getModules(@Context UriInfo uriInfo); + + /** + * Get all modules supported by mount point. + * + * @param identifier + * mount point identifier + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#readData(String, + * UriInfo) + */ + @Deprecated + @GET + @Path("/modules/{identifier:.+}") + @Produces({ + Draft02.MediaTypes.API + JSON, + Draft02.MediaTypes.API + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext getModules(@PathParam("identifier") String identifier, @Context UriInfo uriInfo); + + /** + * Get module. + * + * @param identifier + * path to target + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#readData(String, + * UriInfo) + */ + @Deprecated + @GET + @Path("/modules/module/{identifier:.+}") + @Produces({ + Draft02.MediaTypes.API + JSON, + Draft02.MediaTypes.API + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext getModule(@PathParam("identifier") String identifier, @Context UriInfo uriInfo); + + /** + * List of rpc or action operations supported by the server. + * + * @return A JSON document string + * @deprecated do not use this method. It will be replaced by + * RestconfOperationsService#getOperations(UriInfo) + */ + @Deprecated + @GET + @Path("/operations") + @Produces({ Draft02.MediaTypes.API + JSON, MediaType.APPLICATION_JSON }) + String getOperationsJSON(); + + /** + * List of rpc or action operations supported by the server. + * + * @return A XML document string + * @deprecated do not use this method. It will be replaced by + * RestconfOperationsService#getOperations(UriInfo) + */ + @Deprecated + @GET + @Path("/operations") + @Produces({ Draft02.MediaTypes.API + XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) + String getOperationsXML(); + + /** + * Valid for mount points. List of operations supported by the server. + * + * @param identifier + * path parameter + * @param uriInfo + * URI information + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by + * RestconfOperationsService#getOperations(String, UriInfo) + */ + @Deprecated + @GET + @Path("/operations/{identifier:.+}") + @Produces({ + Draft02.MediaTypes.API + JSON, + Draft02.MediaTypes.API + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext getOperations(@PathParam("identifier") String identifier, @Context UriInfo uriInfo); + + /** + * Invoke RPC operation. + * + * @param identifier + * module name and rpc identifier string for the desired operation + * @param payload + * {@link NormalizedNodeContext} - the body of the operation + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by + * RestconfInvokeOperationsService#invokeRpc(String, NormalizedNodeContext, UriInfo) + */ + @Deprecated + @POST + @Path("/operations/{identifier:.+}") + @Produces({ + Draft02.MediaTypes.OPERATION + JSON, + Draft02.MediaTypes.OPERATION + XML, + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + @Consumes({ + Draft02.MediaTypes.OPERATION + JSON, + Draft02.MediaTypes.OPERATION + XML, + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext invokeRpc(@Encoded @PathParam("identifier") String identifier, NormalizedNodeContext payload, + @Context UriInfo uriInfo); + + /** + * Get target data resource from config data store. + * + * @param identifier + * path to target + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#readData(String, + * UriInfo) + */ + @Deprecated + @GET + @Path("/config/{identifier:.+}") + @Produces({ + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext readConfigurationData(@Encoded @PathParam("identifier") String identifier, + @Context UriInfo uriInfo); + + /** + * Get target data resource from operational data store. + * + * @param identifier + * path to target + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#readData(String, + * UriInfo) + */ + @Deprecated + @GET + @Path("/operational/{identifier:.+}") + @Produces({ + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext readOperationalData(@Encoded @PathParam("identifier") String identifier, + @Context UriInfo uriInfo); + + /** + * Create or replace the target data resource. + * + * @param identifier + * path to target + * @param payload + * data node for put to config DS + * @return {@link Response} + * @deprecated do not use this method. It will be replaced by RestconfDataService#putData(String, + * NormalizedNodeContext, UriInfo) + */ + @Deprecated + @PUT + @Path("/config/{identifier:.+}") + @Consumes({ + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + Response updateConfigurationData(@Encoded @PathParam("identifier") String identifier, + NormalizedNodeContext payload, @Context UriInfo uriInfo); + + /** + * Create a data resource in target. + * + * @param identifier + * path to target + * @param payload + * new data + * @param uriInfo + * URI info + * @return {@link Response} + * @deprecated do not use this method. It will be replaced by RestconfDataService#postData(String, + * NormalizedNodeContext, UriInfo) + */ + @Deprecated + @POST + @Path("/config/{identifier:.+}") + @Consumes({ + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + Response createConfigurationData(@Encoded @PathParam("identifier") String identifier, NormalizedNodeContext payload, + @Context UriInfo uriInfo); + + /** + * Create a data resource. + * + * @param payload + * new data + * @param uriInfo + * URI info + * @return {@link Response} + * @deprecated do not use this method. It will be replaced by + * RestconfDataService#postData(NormalizedNodeContext, UriInfo) + */ + @Deprecated + @POST + @Path("/config") + @Consumes({ + Draft02.MediaTypes.DATA + JSON, + Draft02.MediaTypes.DATA + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + Response createConfigurationData(NormalizedNodeContext payload, @Context UriInfo uriInfo); + + /** + * Delete the target data resource. + * + * @param identifier + * path to target + * @return {@link Response} + * @deprecated do not use this method. It will be replaced by RestconfDataService#deleteData(String) + */ + @Deprecated + @DELETE + @Path("/config/{identifier:.+}") + Response deleteConfigurationData(@Encoded @PathParam("identifier") String identifier); + + /** + * Subscribe to stream. + * + * @param identifier + * stream identifier + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by + * RestconfStreamsSubscriptionService#subscribeToStream(String, UriInfo) + */ + @Deprecated + @GET + @Path("/streams/stream/{identifier:.+}") + NormalizedNodeContext subscribeToStream(@Encoded @PathParam("identifier") String identifier, + @Context UriInfo uriInfo); + + /** + * Get list of all streams. + * + * @param uriInfo + * URI info + * @return {@link NormalizedNodeContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#readData(String, + * UriInfo) + **/ + @Deprecated + @GET + @Path("/streams") + @Produces({ + Draft02.MediaTypes.API + JSON, + Draft02.MediaTypes.API + XML, + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML + }) + NormalizedNodeContext getAvailableStreams(@Context UriInfo uriInfo); + + /** + * Ordered list of edits that are applied to the target datastore by the server. + * + * @param identifier + * path to target + * @param context + * edits + * @param uriInfo + * URI info + * @return {@link PatchStatusContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#patchData(String, + * PatchContext, UriInfo) + */ + @Deprecated + @Patch + @Path("/config/{identifier:.+}") + @Consumes({ + MediaTypes.PATCH + JSON, + MediaTypes.PATCH + XML + }) + @Produces({ + MediaTypes.PATCH_STATUS + JSON, + MediaTypes.PATCH_STATUS + XML + }) + PatchStatusContext patchConfigurationData(@Encoded @PathParam("identifier") String identifier, PatchContext + context, @Context UriInfo uriInfo); + + /** + * Ordered list of edits that are applied to the datastore by the server. + * + * @param context + * edits + * @param uriInfo + * URI info + * @return {@link PatchStatusContext} + * @deprecated do not use this method. It will be replaced by RestconfDataService#patchData(PatchContext, + * UriInfo) + */ + @Deprecated + @Patch + @Path("/config") + @Consumes({ + MediaTypes.PATCH + JSON, + MediaTypes.PATCH + XML + }) + @Produces({ + MediaTypes.PATCH_STATUS + JSON, + MediaTypes.PATCH_STATUS + XML + }) + PatchStatusContext patchConfigurationData(PatchContext context, @Context UriInfo uriInfo); +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/package-info.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/package-info.java new file mode 100644 index 0000000..b765331 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/api/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.api; + diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/AbstractIdentifierAwareJaxRsProvider.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/AbstractIdentifierAwareJaxRsProvider.java new file mode 100644 index 0000000..76ffdb4 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/AbstractIdentifierAwareJaxRsProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.netconf.sal.rest.api.RestconfConstants; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; + +/** + * JAX-RS Provider. + * + * @deprecated This class will be replaced by AbstractIdentifierAwareJaxRsProvider in restconf-nb-rfc8040 + */ +@Deprecated +public class AbstractIdentifierAwareJaxRsProvider { + + private static final String POST = "POST"; + + @Context + private UriInfo uriInfo; + + @Context + private Request request; + + private final ControllerContext controllerContext; + + protected AbstractIdentifierAwareJaxRsProvider(final ControllerContext controllerContext) { + this.controllerContext = controllerContext; + } + + protected final String getIdentifier() { + return uriInfo.getPathParameters(false).getFirst(RestconfConstants.IDENTIFIER); + } + + protected InstanceIdentifierContext getInstanceIdentifierContext() { + return controllerContext.toInstanceIdentifier(getIdentifier()); + } + + protected UriInfo getUriInfo() { + return uriInfo; + } + + protected boolean isPost() { + return POST.equals(request.getMethod()); + } + + protected ControllerContext getControllerContext() { + return controllerContext; + } + + Request getRequest() { + return request; + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriter.java new file mode 100644 index 0000000..b9d7a7a --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriter.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static java.util.Objects.requireNonNull; +import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter.UNKNOWN_SIZE; + +import com.google.common.collect.Iterables; +import java.io.IOException; +import java.util.Collection; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import javax.xml.transform.dom.DOMSource; +import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode; +import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode; +import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +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.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode; +import org.opendaylight.yangtools.yang.data.api.schema.UserLeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an experimental iterator over a {@link NormalizedNode}. This is essentially the opposite of a + * {@link javax.xml.stream.XMLStreamReader} -- unlike instantiating an iterator over the backing data, this + * encapsulates a {@link NormalizedNodeStreamWriter} and allows us to write multiple nodes. + * + * @deprecated This class will be replaced by ParameterAwareNormalizedNodeWriter in restconf-nb-rfc8040 + */ +@Deprecated +public class DepthAwareNormalizedNodeWriter implements RestconfNormalizedNodeWriter { + private final NormalizedNodeStreamWriter writer; + protected int currentDepth = 0; + protected final int maxDepth; + + private DepthAwareNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final int maxDepth) { + this.writer = requireNonNull(writer); + this.maxDepth = maxDepth; + } + + protected final NormalizedNodeStreamWriter getWriter() { + return writer; + } + + /** + * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. + * + * @param writer Back-end writer + * @return A new instance. + */ + public static DepthAwareNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer, + final int maxDepth) { + return forStreamWriter(writer, true, maxDepth); + } + + /** + * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. + * Unlike the simple {@link #forStreamWriter(NormalizedNodeStreamWriter, int)} + * method, this allows the caller to switch off RFC6020 XML compliance, providing better + * throughput. The reason is that the XML mapping rules in RFC6020 require the encoding + * to emit leaf nodes which participate in a list's key first and in the order in which + * they are defined in the key. For JSON, this requirement is completely relaxed and leaves + * can be ordered in any way we see fit. The former requires a bit of work: first a lookup + * for each key and then for each emitted node we need to check whether it was already + * emitted. + * + * @param writer Back-end writer + * @param orderKeyLeaves whether the returned instance should be RFC6020 XML compliant. + * @return A new instance. + */ + public static DepthAwareNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer, + final boolean orderKeyLeaves, final int maxDepth) { + return orderKeyLeaves ? new OrderedDepthAwareNormalizedNodeWriter(writer, maxDepth) + : new DepthAwareNormalizedNodeWriter(writer, maxDepth); + } + + /** + * Iterate over the provided {@link NormalizedNode} and emit write + * events to the encapsulated {@link NormalizedNodeStreamWriter}. + * + * @param node Node + * @return DepthAwareNormalizedNodeWriter + * @throws IOException when thrown from the backing writer. + */ + @Override + public final DepthAwareNormalizedNodeWriter write(final NormalizedNode node) throws IOException { + if (wasProcessedAsCompositeNode(node)) { + return this; + } + + if (wasProcessAsSimpleNode(node)) { + return this; + } + + throw new IllegalStateException("It wasn't possible to serialize node " + node); + } + + @Override + public void flush() throws IOException { + writer.flush(); + } + + @Override + public void close() throws IOException { + writer.flush(); + writer.close(); + } + + /** + * Emit a best guess of a hint for a particular set of children. It evaluates the + * iterable to see if the size can be easily gotten to. If it is, we hint at the + * real number of child nodes. Otherwise we emit UNKNOWN_SIZE. + * + * @param children Child nodes + * @return Best estimate of the collection size required to hold all the children. + */ + static final int childSizeHint(final Iterable<?> children) { + return children instanceof Collection ? ((Collection<?>) children).size() : UNKNOWN_SIZE; + } + + private boolean wasProcessAsSimpleNode(final NormalizedNode node) throws IOException { + if (node instanceof LeafSetEntryNode) { + if (currentDepth < maxDepth) { + final LeafSetEntryNode<?> nodeAsLeafList = (LeafSetEntryNode<?>) node; + writer.startLeafSetEntryNode(nodeAsLeafList.getIdentifier()); + writer.scalarValue(nodeAsLeafList.body()); + writer.endNode(); + } + return true; + } else if (node instanceof LeafNode) { + final LeafNode<?> nodeAsLeaf = (LeafNode<?>)node; + writer.startLeafNode(nodeAsLeaf.getIdentifier()); + writer.scalarValue(nodeAsLeaf.body()); + writer.endNode(); + return true; + } else if (node instanceof AnyxmlNode) { + final AnyxmlNode<?> anyxmlNode = (AnyxmlNode<?>)node; + final Class<?> objectModel = anyxmlNode.bodyObjectModel(); + if (writer.startAnyxmlNode(anyxmlNode.getIdentifier(), objectModel)) { + if (DOMSource.class.isAssignableFrom(objectModel)) { + writer.domSourceValue((DOMSource) anyxmlNode.body()); + } else { + writer.scalarValue(anyxmlNode.body()); + } + writer.endNode(); + } + return true; + } + + return false; + } + + /** + * Emit events for all children and then emit an endNode() event. + * + * @param children Child iterable + * @return True + * @throws IOException when the writer reports it + */ + protected final boolean writeChildren(final Iterable<? extends NormalizedNode> children) throws IOException { + if (currentDepth < maxDepth) { + for (final NormalizedNode child : children) { + write(child); + } + } + writer.endNode(); + return true; + } + + protected boolean writeMapEntryChildren(final MapEntryNode mapEntryNode) throws IOException { + if (currentDepth < maxDepth) { + writeChildren(mapEntryNode.body()); + } else if (currentDepth == maxDepth) { + writeOnlyKeys(mapEntryNode.getIdentifier().entrySet()); + } + return true; + } + + private void writeOnlyKeys(final Set<Entry<QName, Object>> entries) throws IOException { + for (final Entry<QName, Object> entry : entries) { + writer.startLeafNode(new NodeIdentifier(entry.getKey())); + writer.scalarValue(entry.getValue()); + writer.endNode(); + } + writer.endNode(); + } + + protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException { + writer.startMapEntryNode(node.getIdentifier(), childSizeHint(node.body())); + currentDepth++; + writeMapEntryChildren(node); + currentDepth--; + return true; + } + + private boolean wasProcessedAsCompositeNode(final NormalizedNode node) throws IOException { + boolean processedAsCompositeNode = false; + if (node instanceof ContainerNode) { + final ContainerNode n = (ContainerNode) node; + writer.startContainerNode(n.getIdentifier(), childSizeHint(n.body())); + currentDepth++; + processedAsCompositeNode = writeChildren(n.body()); + currentDepth--; + } else if (node instanceof MapEntryNode) { + processedAsCompositeNode = writeMapEntryNode((MapEntryNode) node); + } else if (node instanceof UnkeyedListEntryNode) { + final UnkeyedListEntryNode n = (UnkeyedListEntryNode) node; + writer.startUnkeyedListItem(n.getIdentifier(), childSizeHint(n.body())); + currentDepth++; + processedAsCompositeNode = writeChildren(n.body()); + currentDepth--; + } else if (node instanceof ChoiceNode) { + final ChoiceNode n = (ChoiceNode) node; + writer.startChoiceNode(n.getIdentifier(), childSizeHint(n.body())); + processedAsCompositeNode = writeChildren(n.body()); + } else if (node instanceof AugmentationNode) { + final AugmentationNode n = (AugmentationNode) node; + writer.startAugmentationNode(n.getIdentifier()); + processedAsCompositeNode = writeChildren(n.body()); + } else if (node instanceof UnkeyedListNode) { + final UnkeyedListNode n = (UnkeyedListNode) node; + writer.startUnkeyedList(n.getIdentifier(), childSizeHint(n.body())); + processedAsCompositeNode = writeChildren(n.body()); + } else if (node instanceof UserMapNode) { + final UserMapNode n = (UserMapNode) node; + writer.startOrderedMapNode(n.getIdentifier(), childSizeHint(n.body())); + processedAsCompositeNode = writeChildren(n.body()); + } else if (node instanceof MapNode) { + final MapNode n = (MapNode) node; + writer.startMapNode(n.getIdentifier(), childSizeHint(n.body())); + processedAsCompositeNode = writeChildren(n.body()); + } else if (node instanceof LeafSetNode) { + final LeafSetNode<?> n = (LeafSetNode<?>) node; + if (node instanceof UserLeafSetNode) { + writer.startOrderedLeafSet(n.getIdentifier(), childSizeHint(n.body())); + } else { + writer.startLeafSet(n.getIdentifier(), childSizeHint(n.body())); + } + currentDepth++; + processedAsCompositeNode = writeChildren(n.body()); + currentDepth--; + } + + return processedAsCompositeNode; + } + + private static final class OrderedDepthAwareNormalizedNodeWriter extends DepthAwareNormalizedNodeWriter { + private static final Logger LOG = LoggerFactory.getLogger(OrderedDepthAwareNormalizedNodeWriter.class); + + OrderedDepthAwareNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final int maxDepth) { + super(writer, maxDepth); + } + + @Override + protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException { + final NormalizedNodeStreamWriter writer = getWriter(); + writer.startMapEntryNode(node.getIdentifier(), childSizeHint(node.body())); + + final Set<QName> qnames = node.getIdentifier().keySet(); + // Write out all the key children + for (final QName qname : qnames) { + final Optional<? extends NormalizedNode> child = node.findChildByArg(new NodeIdentifier(qname)); + if (child.isPresent()) { + write(child.get()); + } else { + LOG.info("No child for key element {} found", qname); + } + } + + // Write all the rest + currentDepth++; + final boolean result = writeChildren(Iterables.filter(node.body(), input -> { + if (input instanceof AugmentationNode) { + return true; + } + if (!qnames.contains(input.getIdentifier().getNodeType())) { + return true; + } + + LOG.debug("Skipping key child {}", input); + return false; + })); + currentDepth--; + return result; + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java new file mode 100644 index 0000000..aecea7a --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.google.gson.stream.JsonReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.util.RestUtil; +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.api.schema.AugmentationNode; +import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Provider +@Consumes({ + Draft02.MediaTypes.DATA + RestconfService.JSON, + Draft02.MediaTypes.OPERATION + RestconfService.JSON, + MediaType.APPLICATION_JSON +}) +public class JsonNormalizedNodeBodyReader + extends AbstractIdentifierAwareJaxRsProvider implements MessageBodyReader<NormalizedNodeContext> { + + private static final Logger LOG = LoggerFactory.getLogger(JsonNormalizedNodeBodyReader.class); + + public JsonNormalizedNodeBodyReader(final ControllerContext controllerContext) { + super(controllerContext); + } + + @Override + public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType) { + return true; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public NormalizedNodeContext readFrom(final Class<NormalizedNodeContext> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws + WebApplicationException { + try { + return readFrom(getInstanceIdentifierContext(), entityStream, isPost()); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + public static NormalizedNodeContext readFrom(final String uriPath, final InputStream entityStream, + final boolean isPost, final ControllerContext controllerContext) throws RestconfDocumentedException { + + try { + return readFrom(controllerContext.toInstanceIdentifier(uriPath), entityStream, isPost); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + private static NormalizedNodeContext readFrom(final InstanceIdentifierContext path, + final InputStream entityStream, final boolean isPost) + throws IOException { + final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream); + if (nonEmptyInputStreamOptional.isEmpty()) { + return new NormalizedNodeContext(path, null); + } + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + + final Inference parentInference; + if (isPost && !(path.getSchemaNode() instanceof RpcDefinition)) { + parentInference = path.inference(); + } else { + final var inference = path.inference(); + if (!inference.statementPath().isEmpty()) { + final var stack = inference.toSchemaInferenceStack(); + stack.exit(); + parentInference = stack.toInference(); + } else { + parentInference = inference; + } + } + + final JsonParserStream jsonParser = JsonParserStream.create(writer, + JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(path.getSchemaContext()), + parentInference); + final JsonReader reader = new JsonReader(new InputStreamReader(nonEmptyInputStreamOptional.get(), + StandardCharsets.UTF_8)); + jsonParser.parse(reader); + + NormalizedNode result = resultHolder.getResult(); + final List<YangInstanceIdentifier.PathArgument> iiToDataList = new ArrayList<>(); + InstanceIdentifierContext newIIContext; + + while (result instanceof AugmentationNode || result instanceof ChoiceNode) { + final Object childNode = ((DataContainerNode) result).body().iterator().next(); + if (isPost) { + iiToDataList.add(result.getIdentifier()); + } + result = (NormalizedNode) childNode; + } + + if (isPost) { + if (result instanceof MapEntryNode) { + iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(result.getIdentifier().getNodeType())); + iiToDataList.add(result.getIdentifier()); + } else { + iiToDataList.add(result.getIdentifier()); + } + } else { + if (result instanceof MapNode) { + result = Iterables.getOnlyElement(((MapNode) result).body()); + } + } + + return new NormalizedNodeContext(path.withConcatenatedArgs(iiToDataList), result); + } + + private static void propagateExceptionAs(final Exception exception) throws RestconfDocumentedException { + Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class); + LOG.debug("Error parsing json input", exception); + + if (exception instanceof ResultAlreadySetException) { + throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. " + + "Are you creating multiple resources/subresources in POST request?", exception); + } + + RestconfDocumentedException.throwIfYangError(exception); + throw new RestconfDocumentedException("Error parsing input: " + exception.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, exception); + } +} + diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java new file mode 100644 index 0000000..ce5a8c4 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static com.google.common.base.Verify.verify; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchEditOperation; +import org.opendaylight.restconf.common.patch.PatchEntity; +import org.opendaylight.restconf.common.util.RestUtil; +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.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; +import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Patch reader for JSON. + * + * @deprecated This class will be replaced by JsonToPatchBodyReader in restconf-nb-rfc8040 + */ +@Deprecated +@Provider +@Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON}) +public class JsonToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider + implements MessageBodyReader<PatchContext> { + + private static final Logger LOG = LoggerFactory.getLogger(JsonToPatchBodyReader.class); + + public JsonToPatchBodyReader(final ControllerContext controllerContext) { + super(controllerContext); + } + + @Override + public boolean isReadable(final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return true; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public PatchContext readFrom(final Class<PatchContext> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) + throws WebApplicationException { + try { + return readFrom(getInstanceIdentifierContext(), entityStream); + } catch (final Exception e) { + throw propagateExceptionAs(e); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + public PatchContext readFrom(final String uriPath, final InputStream entityStream) throws + RestconfDocumentedException { + try { + return readFrom(getControllerContext().toInstanceIdentifier(uriPath), entityStream); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + private PatchContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream) + throws IOException { + final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream); + if (nonEmptyInputStreamOptional.isEmpty()) { + return new PatchContext(path, null, null); + } + + final JsonReader jsonReader = new JsonReader(new InputStreamReader(nonEmptyInputStreamOptional.get(), + StandardCharsets.UTF_8)); + AtomicReference<String> patchId = new AtomicReference<>(); + final List<PatchEntity> resultList = read(jsonReader, path, patchId); + jsonReader.close(); + + return new PatchContext(path, resultList, patchId.get()); + } + + private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException { + Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class); + LOG.debug("Error parsing json input", exception); + + if (exception instanceof ResultAlreadySetException) { + throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. "); + } + + RestconfDocumentedException.throwIfYangError(exception); + throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, exception); + } + + private List<PatchEntity> read(final JsonReader in, final InstanceIdentifierContext path, + final AtomicReference<String> patchId) throws IOException { + final List<PatchEntity> resultCollection = new ArrayList<>(); + final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec( + path.getSchemaContext()); + final JsonToPatchBodyReader.PatchEdit edit = new JsonToPatchBodyReader.PatchEdit(); + + while (in.hasNext()) { + switch (in.peek()) { + case STRING: + case NUMBER: + in.nextString(); + break; + case BOOLEAN: + Boolean.toString(in.nextBoolean()); + break; + case NULL: + in.nextNull(); + break; + case BEGIN_ARRAY: + in.beginArray(); + break; + case BEGIN_OBJECT: + in.beginObject(); + break; + case END_DOCUMENT: + break; + case NAME: + parseByName(in.nextName(), edit, in, path, codec, resultCollection, patchId); + break; + case END_OBJECT: + in.endObject(); + break; + case END_ARRAY: + in.endArray(); + break; + + default: + break; + } + } + + return ImmutableList.copyOf(resultCollection); + } + + /** + * Switch value of parsed JsonToken.NAME and read edit definition or patch id. + * + * @param name value of token + * @param edit PatchEdit instance + * @param in JsonReader reader + * @param path InstanceIdentifierContext context + * @param codec StringModuleInstanceIdentifierCodec codec + * @param resultCollection collection of parsed edits + * @throws IOException if operation fails + */ + private void parseByName(final @NonNull String name, final @NonNull PatchEdit edit, + final @NonNull JsonReader in, final @NonNull InstanceIdentifierContext path, + final @NonNull StringModuleInstanceIdentifierCodec codec, + final @NonNull List<PatchEntity> resultCollection, + final @NonNull AtomicReference<String> patchId) throws IOException { + switch (name) { + case "edit" : + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + + while (in.hasNext()) { + readEditDefinition(edit, in, path, codec); + resultCollection.add(prepareEditOperation(edit)); + edit.clear(); + } + + in.endArray(); + } else { + readEditDefinition(edit, in, path, codec); + resultCollection.add(prepareEditOperation(edit)); + edit.clear(); + } + + break; + case "patch-id" : + patchId.set(in.nextString()); + break; + default: + break; + } + } + + /** + * Read one patch edit object from Json input. + * @param edit PatchEdit instance to be filled with read data + * @param in JsonReader reader + * @param path InstanceIdentifierContext path context + * @param codec StringModuleInstanceIdentifierCodec codec + * @throws IOException if operation fails + */ + private void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in, + final @NonNull InstanceIdentifierContext path, + final @NonNull StringModuleInstanceIdentifierCodec codec) throws IOException { + final StringBuilder value = new StringBuilder(); + in.beginObject(); + + while (in.hasNext()) { + final String editDefinition = in.nextName(); + switch (editDefinition) { + case "edit-id" : + edit.setId(in.nextString()); + break; + case "operation" : + edit.setOperation(PatchEditOperation.valueOf(in.nextString().toUpperCase(Locale.ROOT))); + break; + case "target" : + // target can be specified completely in request URI + final String target = in.nextString(); + if (target.equals("/")) { + edit.setTarget(path.getInstanceIdentifier()); + edit.setTargetSchemaNode(SchemaInferenceStack.of(path.getSchemaContext()).toInference()); + } else { + edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()).concat(target))); + + final var stack = codec.getDataContextTree().enterPath(edit.getTarget()).orElseThrow().stack(); + stack.exit(); + final EffectiveStatement<?, ?> parentStmt = stack.currentStatement(); + verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt); + edit.setTargetSchemaNode(stack.toInference()); + } + + break; + case "value" : + // save data defined in value node for next (later) processing, because target needs to be read + // always first and there is no ordering in Json input + readValueNode(value, in); + break; + default: + break; + } + } + + in.endObject(); + + // read saved data to normalized node when target schema is already known + edit.setData(readEditData(new JsonReader( + new StringReader(value.toString())), edit.getTargetSchemaNode(), path)); + } + + /** + * Parse data defined in value node and saves it to buffer. + * @param value Buffer to read value node + * @param in JsonReader reader + * @throws IOException if operation fails + */ + private void readValueNode(final @NonNull StringBuilder value, final @NonNull JsonReader in) throws IOException { + in.beginObject(); + value.append('{'); + + value.append('"').append(in.nextName()).append("\":"); + + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + value.append('['); + + while (in.hasNext()) { + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + } else { + readValueObject(value, in); + } + if (in.peek() != JsonToken.END_ARRAY) { + value.append(','); + } + } + + in.endArray(); + value.append(']'); + } else { + readValueObject(value, in); + } + + in.endObject(); + value.append('}'); + } + + /** + * Parse one value object of data and saves it to buffer. + * @param value Buffer to read value object + * @param in JsonReader reader + * @throws IOException if operation fails + */ + private void readValueObject(final @NonNull StringBuilder value, final @NonNull JsonReader in) throws IOException { + // read simple leaf value + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + return; + } + + in.beginObject(); + value.append('{'); + + while (in.hasNext()) { + value.append('"').append(in.nextName()).append("\":"); + + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + } else { + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + value.append('['); + + while (in.hasNext()) { + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + } else { + readValueObject(value, in); + } + if (in.peek() != JsonToken.END_ARRAY) { + value.append(','); + } + } + + in.endArray(); + value.append(']'); + } else { + readValueObject(value, in); + } + } + + if (in.peek() != JsonToken.END_OBJECT) { + value.append(','); + } + } + + in.endObject(); + value.append('}'); + } + + /** + * Read patch edit data defined in value node to NormalizedNode. + * @param in reader JsonReader reader + * @return NormalizedNode representing data + */ + private static NormalizedNode readEditData(final @NonNull JsonReader in, + final @NonNull Inference targetSchemaNode, final @NonNull InstanceIdentifierContext path) { + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + final EffectiveModelContext context = path.getSchemaContext(); + JsonParserStream.create(writer, JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context), + targetSchemaNode) + .parse(in); + + return resultHolder.getResult(); + } + + /** + * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception. + * @param edit Instance of PatchEdit + * @return PatchEntity Patch entity + */ + private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) { + if (edit.getOperation() != null && edit.getTargetSchemaNode() != null + && checkDataPresence(edit.getOperation(), edit.getData() != null)) { + if (edit.getOperation().isWithValue()) { + // for lists allow to manipulate with list items through their parent + final YangInstanceIdentifier targetNode; + if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) { + targetNode = edit.getTarget().getParent(); + } else { + targetNode = edit.getTarget(); + } + + return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData()); + } + + return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget()); + } + + throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + /** + * Check if data is present when operation requires it and not present when operation data is not allowed. + * @param operation Name of operation + * @param hasData Data in edit are present/not present + * @return true if data is present when operation requires it or if there are no data when operation does not + * allow it, false otherwise + */ + private static boolean checkDataPresence(final @NonNull PatchEditOperation operation, final boolean hasData) { + return operation.isWithValue() == hasData; + } + + /** + * Helper class representing one patch edit. + */ + private static final class PatchEdit { + private String id; + private PatchEditOperation operation; + private YangInstanceIdentifier target; + private Inference targetSchemaNode; + private NormalizedNode data; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public PatchEditOperation getOperation() { + return operation; + } + + public void setOperation(final PatchEditOperation operation) { + this.operation = operation; + } + + public YangInstanceIdentifier getTarget() { + return target; + } + + public void setTarget(final YangInstanceIdentifier target) { + this.target = target; + } + + public Inference getTargetSchemaNode() { + return targetSchemaNode; + } + + public void setTargetSchemaNode(final Inference targetSchemaNode) { + this.targetSchemaNode = targetSchemaNode; + } + + public NormalizedNode getData() { + return data; + } + + public void setData(final NormalizedNode data) { + this.data = data; + } + + public void clear() { + id = null; + operation = null; + target = null; + targetSchemaNode = null; + data = null; + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeContext.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeContext.java new file mode 100644 index 0000000..d52de7d --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeContext.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ImmutableMap; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +@Deprecated(forRemoval = true, since = "2.0.6") +// Non-final for mocking +public class NormalizedNodeContext { + private final InstanceIdentifierContext context; + private final ImmutableMap<String, Object> headers; + private final WriterParameters writerParameters; + private final NormalizedNode data; + + public NormalizedNodeContext(final InstanceIdentifierContext context, + final NormalizedNode data, final WriterParameters writerParameters, + final ImmutableMap<String, Object> headers) { + this.context = context; + this.data = data; + this.writerParameters = writerParameters; + this.headers = requireNonNull(headers); + } + + public NormalizedNodeContext(final InstanceIdentifierContext context, + final NormalizedNode data, final WriterParameters writerParameters) { + this(context, data, writerParameters, ImmutableMap.of()); + } + + public NormalizedNodeContext(final InstanceIdentifierContext context, + final NormalizedNode data) { + this(context, data, WriterParameters.EMPTY, ImmutableMap.of()); + } + + public NormalizedNodeContext(final InstanceIdentifierContext context, + final NormalizedNode data, final ImmutableMap<String, Object> headers) { + this(context, data, WriterParameters.EMPTY, headers); + } + + public InstanceIdentifierContext getInstanceIdentifierContext() { + return context; + } + + public NormalizedNode getData() { + return data; + } + + public WriterParameters getWriterParameters() { + return writerParameters; + } + + /** + * Return headers of {@code NormalizedNodeContext}. + * + * @return map of headers + */ + public ImmutableMap<String, Object> getNewHeaders() { + return headers; + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeJsonBodyWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeJsonBodyWriter.java new file mode 100644 index 0000000..f754df8 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeJsonBodyWriter.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Map.Entry; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import javax.xml.stream.XMLStreamException; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.util.NetconfUtil; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DOMSourceAnyxmlNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.xml.sax.SAXException; + +/** + * Normalized node writer for JSON. + * + * @deprecated This class will be replaced by NormalizedNodeJsonBodyWriter from restconf-nb-rfc8040 + */ +@Deprecated +@Provider +@Produces({ + Draft02.MediaTypes.API + RestconfService.JSON, + Draft02.MediaTypes.DATA + RestconfService.JSON, + Draft02.MediaTypes.OPERATION + RestconfService.JSON, + MediaType.APPLICATION_JSON +}) +public class NormalizedNodeJsonBodyWriter implements MessageBodyWriter<NormalizedNodeContext> { + + private static final int DEFAULT_INDENT_SPACES_NUM = 2; + + @Override + public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType) { + return type.equals(NormalizedNodeContext.class); + } + + @Override + public long getSize(final NormalizedNodeContext context, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(final NormalizedNodeContext context, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders, + final OutputStream entityStream) throws IOException, WebApplicationException { + if (httpHeaders != null) { + for (final Entry<String, Object> entry : context.getNewHeaders().entrySet()) { + httpHeaders.add(entry.getKey(), entry.getValue()); + } + } + NormalizedNode data = context.getData(); + if (data == null) { + return; + } + + final InstanceIdentifierContext identifierCtx = context.getInstanceIdentifierContext(); + + try (JsonWriter jsonWriter = createJsonWriter(entityStream, context.getWriterParameters().isPrettyPrint())) { + jsonWriter.beginObject(); + writeNormalizedNode(jsonWriter, identifierCtx, data, context.getWriterParameters().getDepth()); + jsonWriter.endObject(); + jsonWriter.flush(); + } + } + + private static void writeNormalizedNode(final JsonWriter jsonWriter, final InstanceIdentifierContext context, + // Note: mutable argument + NormalizedNode data, final @Nullable Integer depth) throws IOException { + + final var stack = context.inference().toSchemaInferenceStack(); + final RestconfNormalizedNodeWriter nnWriter; + if (stack.isEmpty()) { + /* + * Creates writer without initialNs and we write children of root data container + * which is not visible in restconf + */ + nnWriter = createNormalizedNodeWriter(context, context.inference(), jsonWriter, depth); + if (data instanceof ContainerNode) { + writeChildren(nnWriter,(ContainerNode) data); + } else if (data instanceof DOMSourceAnyxmlNode) { + try { + writeChildren(nnWriter, + (ContainerNode) NetconfUtil.transformDOMSourceToNormalizedNode( + context.getSchemaContext(), ((DOMSourceAnyxmlNode)data).body()).getResult()); + } catch (XMLStreamException | URISyntaxException | SAXException e) { + throw new IOException("Cannot write anyxml.", e); + } + } + } else if (context.getSchemaNode() instanceof RpcDefinition) { + /* + * RpcDefinition is not supported as initial codec in JSONStreamWriter, + * so we need to emit initial output declaratation.. + */ + final var rpc = (RpcDefinition) context.getSchemaNode(); + final var tmp = SchemaInferenceStack.of(context.getSchemaContext()); + tmp.enterSchemaTree(rpc.getQName()); + tmp.enterSchemaTree(rpc.getOutput().getQName()); + + nnWriter = createNormalizedNodeWriter(context, tmp.toInference(), jsonWriter, depth); + jsonWriter.name("output"); + jsonWriter.beginObject(); + writeChildren(nnWriter, (ContainerNode) data); + jsonWriter.endObject(); + } else { + stack.exit(); + + if (data instanceof MapEntryNode) { + data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType()) + .withChild((MapEntryNode) data) + .build(); + } + nnWriter = createNormalizedNodeWriter(context, stack.toInference(), jsonWriter, depth); + nnWriter.write(data); + } + nnWriter.flush(); + } + + private static void writeChildren(final RestconfNormalizedNodeWriter nnWriter, final ContainerNode data) + throws IOException { + for (final DataContainerChild child : data.body()) { + nnWriter.write(child); + } + } + + private static RestconfNormalizedNodeWriter createNormalizedNodeWriter( + final InstanceIdentifierContext context, final Inference inference, final JsonWriter jsonWriter, + final @Nullable Integer depth) { + + final SchemaNode schema = context.getSchemaNode(); + final JSONCodecFactory codecs = getCodecFactory(context); + + final XMLNamespace initialNs; + if (schema instanceof DataSchemaNode && !((DataSchemaNode)schema).isAugmenting() + && !(schema instanceof SchemaContext) || schema instanceof RpcDefinition) { + initialNs = schema.getQName().getNamespace(); + } else { + initialNs = null; + } + final NormalizedNodeStreamWriter streamWriter = + JSONNormalizedNodeStreamWriter.createNestedWriter(codecs, inference, initialNs, jsonWriter); + if (depth != null) { + return DepthAwareNormalizedNodeWriter.forStreamWriter(streamWriter, depth); + } + + return RestconfDelegatingNormalizedNodeWriter.forStreamWriter(streamWriter); + } + + private static JsonWriter createJsonWriter(final OutputStream entityStream, final boolean prettyPrint) { + if (prettyPrint) { + return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, StandardCharsets.UTF_8), + DEFAULT_INDENT_SPACES_NUM); + } + + return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)); + } + + private static JSONCodecFactory getCodecFactory(final InstanceIdentifierContext context) { + // TODO: Performance: Cache JSON Codec factory and schema context + return JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeXmlBodyWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeXmlBodyWriter.java new file mode 100644 index 0000000..e0ba9d9 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeXmlBodyWriter.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Map.Entry; +import javanet.staxutils.IndentingXMLStreamWriter; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import javax.xml.XMLConstants; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.util.NetconfUtil; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DOMSourceAnyxmlNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.xml.sax.SAXException; + +/** + * Normalized node writer for XML. + * + * @deprecated This class will be replaced by NormalizedNodeXmlBodyWriter from restconf-nb-rfc8040 + */ +@Deprecated +@Provider +@Produces({ + Draft02.MediaTypes.API + RestconfService.XML, + Draft02.MediaTypes.DATA + RestconfService.XML, + Draft02.MediaTypes.OPERATION + RestconfService.XML, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML +}) +public class NormalizedNodeXmlBodyWriter implements MessageBodyWriter<NormalizedNodeContext> { + + private static final XMLOutputFactory XML_FACTORY; + + static { + XML_FACTORY = XMLOutputFactory.newFactory(); + XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + } + + @Override + public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType) { + return type.equals(NormalizedNodeContext.class); + } + + @Override + public long getSize(final NormalizedNodeContext context, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(final NormalizedNodeContext context, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException, + WebApplicationException { + for (final Entry<String, Object> entry : context.getNewHeaders().entrySet()) { + httpHeaders.add(entry.getKey(), entry.getValue()); + } + final InstanceIdentifierContext pathContext = context.getInstanceIdentifierContext(); + if (context.getData() == null) { + return; + } + + XMLStreamWriter xmlWriter; + try { + xmlWriter = XML_FACTORY.createXMLStreamWriter(entityStream, StandardCharsets.UTF_8.name()); + if (context.getWriterParameters().isPrettyPrint()) { + xmlWriter = new IndentingXMLStreamWriter(xmlWriter); + } + } catch (final XMLStreamException | FactoryConfigurationError e) { + throw new IllegalStateException(e); + } + final NormalizedNode data = context.getData(); + + writeNormalizedNode(xmlWriter, pathContext.inference().toSchemaInferenceStack(), pathContext, data, + context.getWriterParameters().getDepth()); + } + + private static void writeNormalizedNode(final XMLStreamWriter xmlWriter, final SchemaInferenceStack stack, + final InstanceIdentifierContext pathContext, NormalizedNode data, final @Nullable Integer depth) + throws IOException { + final RestconfNormalizedNodeWriter nnWriter; + final EffectiveModelContext schemaCtx = pathContext.getSchemaContext(); + if (stack.isEmpty()) { + nnWriter = createNormalizedNodeWriter(xmlWriter, pathContext.inference(), depth); + if (data instanceof DOMSourceAnyxmlNode) { + try { + writeElements(xmlWriter, nnWriter, + (ContainerNode) NetconfUtil.transformDOMSourceToNormalizedNode(schemaCtx, + ((DOMSourceAnyxmlNode)data).body()).getResult()); + } catch (XMLStreamException | URISyntaxException | SAXException e) { + throw new IOException("Cannot write anyxml", e); + } + } else { + writeElements(xmlWriter, nnWriter, (ContainerNode) data); + } + } else if (pathContext.getSchemaNode() instanceof RpcDefinition) { + final var rpc = (RpcDefinition) pathContext.getSchemaNode(); + final var tmp = SchemaInferenceStack.of(pathContext.getSchemaContext()); + tmp.enterSchemaTree(rpc.getQName()); + tmp.enterSchemaTree(rpc.getOutput().getQName()); + + nnWriter = createNormalizedNodeWriter(xmlWriter, tmp.toInference(), depth); + writeElements(xmlWriter, nnWriter, (ContainerNode) data); + } else { + stack.exit(); + nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), depth); + if (data instanceof MapEntryNode) { + // Restconf allows returning one list item. We need to wrap it + // in map node in order to serialize it properly + data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType()) + .addChild((MapEntryNode) data) + .build(); + } + nnWriter.write(data); + } + nnWriter.flush(); + } + + private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final XMLStreamWriter xmlWriter, + final Inference inference, final @Nullable Integer depth) { + final NormalizedNodeStreamWriter xmlStreamWriter = + XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, inference); + if (depth != null) { + return DepthAwareNormalizedNodeWriter.forStreamWriter(xmlStreamWriter, depth); + } + + return RestconfDelegatingNormalizedNodeWriter.forStreamWriter(xmlStreamWriter); + } + + private static void writeElements(final XMLStreamWriter xmlWriter, final RestconfNormalizedNodeWriter nnWriter, + final ContainerNode data) throws IOException { + final QName name = data.getIdentifier().getNodeType(); + try { + xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, name.getLocalName(), + name.getNamespace().toString()); + xmlWriter.writeDefaultNamespace(name.getNamespace().toString()); + for (final NormalizedNode child : data.body()) { + nnWriter.write(child); + } + nnWriter.flush(); + xmlWriter.writeEndElement(); + xmlWriter.flush(); + } catch (final XMLStreamException e) { + throw new IOException("Failed to write elements", e); + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/PatchJsonBodyWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/PatchJsonBodyWriter.java new file mode 100644 index 0000000..63586af --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/PatchJsonBodyWriter.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.restconf.common.errors.RestconfError; +import org.opendaylight.restconf.common.patch.PatchStatusContext; +import org.opendaylight.restconf.common.patch.PatchStatusEntity; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory; + + +@Provider +@Produces({ Draft02.MediaTypes.PATCH_STATUS + RestconfService.JSON }) +public class PatchJsonBodyWriter implements MessageBodyWriter<PatchStatusContext> { + + @Override + public boolean isWriteable(final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return type.equals(PatchStatusContext.class); + } + + @Override + public long getSize(final PatchStatusContext patchStatusContext, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(final PatchStatusContext patchStatusContext, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) + throws IOException, WebApplicationException { + + final JsonWriter jsonWriter = createJsonWriter(entityStream); + jsonWriter.beginObject().name("ietf-yang-patch:yang-patch-status"); + jsonWriter.beginObject(); + jsonWriter.name("patch-id").value(patchStatusContext.getPatchId()); + if (patchStatusContext.isOk()) { + reportSuccess(jsonWriter); + } else { + if (patchStatusContext.getGlobalErrors() != null) { + reportErrors(patchStatusContext.getGlobalErrors(), jsonWriter); + } + + jsonWriter.name("edit-status"); + jsonWriter.beginObject(); + jsonWriter.name("edit"); + jsonWriter.beginArray(); + for (final PatchStatusEntity patchStatusEntity : patchStatusContext.getEditCollection()) { + jsonWriter.beginObject(); + jsonWriter.name("edit-id").value(patchStatusEntity.getEditId()); + if (patchStatusEntity.getEditErrors() != null) { + reportErrors(patchStatusEntity.getEditErrors(), jsonWriter); + } else { + if (patchStatusEntity.isOk()) { + reportSuccess(jsonWriter); + } + } + jsonWriter.endObject(); + } + jsonWriter.endArray(); + jsonWriter.endObject(); + } + jsonWriter.endObject(); + jsonWriter.endObject(); + jsonWriter.flush(); + } + + private static void reportSuccess(final JsonWriter jsonWriter) throws IOException { + jsonWriter.name("ok").beginArray().nullValue().endArray(); + } + + private static void reportErrors(final List<RestconfError> errors, final JsonWriter jsonWriter) throws IOException { + jsonWriter.name("errors"); + jsonWriter.beginObject(); + jsonWriter.name("error"); + jsonWriter.beginArray(); + + for (final RestconfError restconfError : errors) { + jsonWriter.beginObject(); + jsonWriter.name("error-type").value(restconfError.getErrorType().elementBody()); + jsonWriter.name("error-tag").value(restconfError.getErrorTag().elementBody()); + + // optional node + if (restconfError.getErrorPath() != null) { + jsonWriter.name("error-path").value(restconfError.getErrorPath().toString()); + } + + // optional node + if (restconfError.getErrorMessage() != null) { + jsonWriter.name("error-message").value(restconfError.getErrorMessage()); + } + + // optional node + if (restconfError.getErrorInfo() != null) { + jsonWriter.name("error-info").value(restconfError.getErrorInfo()); + } + + jsonWriter.endObject(); + } + + jsonWriter.endArray(); + jsonWriter.endObject(); + } + + private static JsonWriter createJsonWriter(final OutputStream entityStream) { + return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/PatchXmlBodyWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/PatchXmlBodyWriter.java new file mode 100644 index 0000000..d941d6c --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/PatchXmlBodyWriter.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.restconf.common.errors.RestconfError; +import org.opendaylight.restconf.common.patch.PatchStatusContext; +import org.opendaylight.restconf.common.patch.PatchStatusEntity; + +@Provider +@Produces({ Draft02.MediaTypes.PATCH_STATUS + RestconfService.XML }) +public class PatchXmlBodyWriter implements MessageBodyWriter<PatchStatusContext> { + + private static final XMLOutputFactory XML_FACTORY; + + static { + XML_FACTORY = XMLOutputFactory.newFactory(); + XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + } + + @Override + public boolean isWriteable(final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return type.equals(PatchStatusContext.class); + } + + @Override + public long getSize(final PatchStatusContext patchStatusContext, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(final PatchStatusContext patchStatusContext, final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) + throws WebApplicationException { + + try { + final XMLStreamWriter xmlWriter = + XML_FACTORY.createXMLStreamWriter(entityStream, StandardCharsets.UTF_8.name()); + writeDocument(xmlWriter, patchStatusContext); + } catch (final XMLStreamException | FactoryConfigurationError e) { + throw new IllegalStateException(e); + } + } + + private static void writeDocument(final XMLStreamWriter writer, final PatchStatusContext context) + throws XMLStreamException { + writer.writeStartElement("", "yang-patch-status", "urn:ietf:params:xml:ns:yang:ietf-yang-patch"); + writer.writeStartElement("patch-id"); + writer.writeCharacters(context.getPatchId()); + writer.writeEndElement(); + + if (context.isOk()) { + writer.writeEmptyElement("ok"); + } else { + if (context.getGlobalErrors() != null) { + reportErrors(context.getGlobalErrors(), writer); + } + writer.writeStartElement("edit-status"); + for (final PatchStatusEntity patchStatusEntity : context.getEditCollection()) { + writer.writeStartElement("edit"); + writer.writeStartElement("edit-id"); + writer.writeCharacters(patchStatusEntity.getEditId()); + writer.writeEndElement(); + if (patchStatusEntity.getEditErrors() != null) { + reportErrors(patchStatusEntity.getEditErrors(), writer); + } else { + if (patchStatusEntity.isOk()) { + writer.writeEmptyElement("ok"); + } + } + writer.writeEndElement(); + } + writer.writeEndElement(); + + } + writer.writeEndElement(); + + writer.flush(); + } + + private static void reportErrors(final List<RestconfError> errors, final XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement("errors"); + + for (final RestconfError restconfError : errors) { + writer.writeStartElement("error-type"); + writer.writeCharacters(restconfError.getErrorType().elementBody()); + writer.writeEndElement(); + + writer.writeStartElement("error-tag"); + writer.writeCharacters(restconfError.getErrorTag().elementBody()); + writer.writeEndElement(); + + // optional node + if (restconfError.getErrorPath() != null) { + writer.writeStartElement("error-path"); + writer.writeCharacters(restconfError.getErrorPath().toString()); + writer.writeEndElement(); + } + + // optional node + if (restconfError.getErrorMessage() != null) { + writer.writeStartElement("error-message"); + writer.writeCharacters(restconfError.getErrorMessage()); + writer.writeEndElement(); + } + + // optional node + if (restconfError.getErrorInfo() != null) { + writer.writeStartElement("error-info"); + writer.writeCharacters(restconfError.getErrorInfo()); + writer.writeEndElement(); + } + } + + writer.writeEndElement(); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java new file mode 100644 index 0000000..d8918df --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.Application; +import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContentYangBodyWriter; +import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContentYinBodyWriter; +import org.opendaylight.netconf.md.sal.rest.schema.SchemaRetrievalServiceImpl; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.netconf.sal.restconf.impl.StatisticsRestconfServiceWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +@Deprecated(since = "2.0.12") +public class RestconfApplication extends Application { + private static final Logger LOG = LoggerFactory.getLogger(RestconfApplication.class); + + private final ControllerContext controllerContext; + private final StatisticsRestconfServiceWrapper statsServiceWrapper; + + @Inject + public RestconfApplication(final ControllerContext controllerContext, + final StatisticsRestconfServiceWrapper statsServiceWrapper) { + this.controllerContext = controllerContext; + this.statsServiceWrapper = statsServiceWrapper; + LOG.warn("Pre-standard version of RESTCONF activated. Please note that this implementation is considered " + + "obsoleve and WILL BE REMOVED IN THE NEXT MAJOR RELEASE. Please use the RFC8040-compliant " + + "implementation instead."); + } + + @Override + public Set<Class<?>> getClasses() { + return ImmutableSet.<Class<?>>builder() + .add(PatchJsonBodyWriter.class) + .add(PatchXmlBodyWriter.class) + .add(NormalizedNodeJsonBodyWriter.class) + .add(NormalizedNodeXmlBodyWriter.class) + .add(SchemaExportContentYinBodyWriter.class) + .add(SchemaExportContentYangBodyWriter.class) + .build(); + } + + @Override + public Set<Object> getSingletons() { + final Set<Object> singletons = new HashSet<>(); + final SchemaRetrievalServiceImpl schemaRetrieval = new SchemaRetrievalServiceImpl(controllerContext); + singletons.add(schemaRetrieval); + singletons.add(new RestconfCompositeWrapper(statsServiceWrapper, schemaRetrieval)); + singletons.add(new RestconfDocumentedExceptionMapper(controllerContext)); + singletons.add(new XmlNormalizedNodeBodyReader(controllerContext)); + singletons.add(new JsonNormalizedNodeBodyReader(controllerContext)); + singletons.add(new XmlToPatchBodyReader(controllerContext)); + singletons.add(new JsonToPatchBodyReader(controllerContext)); +// singletons.add(StructuredDataToXmlProvider.INSTANCE); +// singletons.add(StructuredDataToJsonProvider.INSTANCE); +// singletons.add(JsonToCompositeNodeProvider.INSTANCE); +// singletons.add(XmlToCompositeNodeProvider.INSTANCE); + return singletons; + } + +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java new file mode 100644 index 0000000..90b83a0 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static java.util.Objects.requireNonNull; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.netconf.md.sal.rest.schema.SchemaRetrievalService; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchStatusContext; +import org.opendaylight.restconf.common.schema.SchemaExportContext; + +public class RestconfCompositeWrapper implements RestconfService, SchemaRetrievalService { + + private final RestconfService restconf; + private final SchemaRetrievalService schema; + + public RestconfCompositeWrapper(final RestconfService restconf, final SchemaRetrievalService schema) { + this.restconf = requireNonNull(restconf); + this.schema = requireNonNull(schema); + } + + @Override + public Object getRoot() { + return this.restconf.getRoot(); + } + + @Override + public NormalizedNodeContext getModules(final UriInfo uriInfo) { + return this.restconf.getModules(uriInfo); + } + + @Override + public NormalizedNodeContext getModules(final String identifier, final UriInfo uriInfo) { + return this.restconf.getModules(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext getModule(final String identifier, final UriInfo uriInfo) { + return this.restconf.getModule(identifier, uriInfo); + } + + @Override + public String getOperationsJSON() { + return this.restconf.getOperationsJSON(); + } + + @Override + public String getOperationsXML() { + return this.restconf.getOperationsXML(); + } + + @Override + public NormalizedNodeContext getOperations(final String identifier, final UriInfo uriInfo) { + return this.restconf.getOperations(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext invokeRpc(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + return this.restconf.invokeRpc(identifier, payload, uriInfo); + } + + @Override + public NormalizedNodeContext readConfigurationData(final String identifier, final UriInfo uriInfo) { + return this.restconf.readConfigurationData(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext readOperationalData(final String identifier, final UriInfo uriInfo) { + return this.restconf.readOperationalData(identifier, uriInfo); + } + + @Override + public Response updateConfigurationData(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + return this.restconf.updateConfigurationData(identifier, payload, uriInfo); + } + + @Override + public Response createConfigurationData(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + return this.restconf.createConfigurationData(identifier, payload, uriInfo); + } + + @Override + public Response createConfigurationData(final NormalizedNodeContext payload, final UriInfo uriInfo) { + return this.restconf.createConfigurationData(payload, uriInfo); + } + + @Override + public Response deleteConfigurationData(final String identifier) { + return this.restconf.deleteConfigurationData(identifier); + } + + @Override + public NormalizedNodeContext subscribeToStream(final String identifier, final UriInfo uriInfo) { + return this.restconf.subscribeToStream(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext getAvailableStreams(final UriInfo uriInfo) { + return this.restconf.getAvailableStreams(uriInfo); + } + + @Override + public PatchStatusContext patchConfigurationData(final String identifier, final PatchContext payload, + final UriInfo uriInfo) { + return this.restconf.patchConfigurationData(identifier, payload, uriInfo); + } + + @Override + public PatchStatusContext patchConfigurationData(final PatchContext context, final UriInfo uriInfo) { + return this.restconf.patchConfigurationData(context, uriInfo); + } + + @Override + public SchemaExportContext getSchema(final String mountId) { + return this.schema.getSchema(mountId); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfDelegatingNormalizedNodeWriter.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfDelegatingNormalizedNodeWriter.java new file mode 100644 index 0000000..aaa1ccc --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfDelegatingNormalizedNodeWriter.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import java.io.IOException; +import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; + +/** + * This class just delegates all of the functionality to Yangtools normalized node writer. + */ +public final class RestconfDelegatingNormalizedNodeWriter implements RestconfNormalizedNodeWriter { + private final NormalizedNodeWriter delegNNWriter; + + private RestconfDelegatingNormalizedNodeWriter(final NormalizedNodeStreamWriter streamWriter, final boolean + orderKeyLeaves) { + this.delegNNWriter = NormalizedNodeWriter.forStreamWriter(streamWriter, orderKeyLeaves); + } + + public static RestconfDelegatingNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer) { + return forStreamWriter(writer, true); + } + + public static RestconfDelegatingNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer, + final boolean orderKeyLeaves) { + return new RestconfDelegatingNormalizedNodeWriter(writer, orderKeyLeaves); + } + + @Override + public RestconfDelegatingNormalizedNodeWriter write(final NormalizedNode node) throws IOException { + delegNNWriter.write(node); + return this; + } + + @Override + public void flush() throws IOException { + delegNNWriter.flush(); + } + + @Override + public void close() throws IOException { + delegNNWriter.close(); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfDocumentedExceptionMapper.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfDocumentedExceptionMapper.java new file mode 100644 index 0000000..ff0be39 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfDocumentedExceptionMapper.java @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +import com.google.gson.stream.JsonWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import javax.xml.XMLConstants; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.ErrorTags; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.errors.RestconfError; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +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.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory; +import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations + * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft. + * + * @author Thomas Pantelis + */ +@Provider +public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> { + + private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class); + + private static final XMLOutputFactory XML_FACTORY; + + static { + XML_FACTORY = XMLOutputFactory.newFactory(); + XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + } + + @Context + private HttpHeaders headers; + + private final ControllerContext controllerContext; + + public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) { + this.controllerContext = requireNonNull(controllerContext); + } + + @Override + public Response toResponse(final RestconfDocumentedException exception) { + + LOG.debug("In toResponse: {}", exception.getMessage()); + + final List<MediaType> mediaTypeList = new ArrayList<>(); + if (headers.getMediaType() != null) { + mediaTypeList.add(headers.getMediaType()); + } + + mediaTypeList.addAll(headers.getAcceptableMediaTypes()); + final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE)) + .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE); + + LOG.debug("Using MediaType: {}", mediaType); + + final List<RestconfError> errors = exception.getErrors(); + if (errors.isEmpty()) { + // We don't actually want to send any content but, if we don't set any content here, + // the tomcat front-end will send back an html error report. To prevent that, set a + // single space char in the entity. + + return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build(); + } + + final Status status = ErrorTags.statusOf(errors.iterator().next().getErrorTag()); + final var errorsEntry = controllerContext.getRestconfModuleErrorsSchemaNode(); + if (errorsEntry == null) { + return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build(); + } + + final var errorsSchemaNode = errorsEntry.getValue(); + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild = + SchemaAwareBuilders.containerBuilder(errorsSchemaNode); + + final var schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode, + Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE); + final DataSchemaNode errListSchemaNode = ControllerContext.getFirst(schemaList); + checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode"); + final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listErorsBuilder = SchemaAwareBuilders + .mapBuilder((ListSchemaNode) errListSchemaNode); + + + for (final RestconfError error : errors) { + listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode)); + } + errContBuild.withChild(listErorsBuilder.build()); + + final NormalizedNodeContext errContext = new NormalizedNodeContext( + InstanceIdentifierContext.ofStack(errorsEntry.getKey(), null), errContBuild.build()); + + final String responseBody; + if (mediaType.getSubtype().endsWith("json")) { + responseBody = toJsonResponseBody(errContext); + } else { + responseBody = toXMLResponseBody(errContext); + } + + return Response.status(status).type(mediaType).entity(responseBody).build(); + } + + private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) { + checkArgument(errListSchemaNode instanceof ListSchemaNode, + "errListSchemaNode has to be of type ListSchemaNode"); + final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode; + final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = SchemaAwareBuilders + .mapEntryBuilder(listStreamSchemaNode); + + var lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + listStreamSchemaNode, "error-type"); + final DataSchemaNode errTypSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode); + checkState(errTypSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTypSchemaNode) + .withValue(error.getErrorType().elementBody()).build()); + + lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + listStreamSchemaNode, "error-tag"); + final DataSchemaNode errTagSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode); + checkState(errTagSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTagSchemaNode) + .withValue(error.getErrorTag().elementBody()).build()); + + if (error.getErrorAppTag() != null) { + lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + listStreamSchemaNode, "error-app-tag"); + final DataSchemaNode errAppTagSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode); + checkState(errAppTagSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode) + .withValue(error.getErrorAppTag()).build()); + } + + lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + listStreamSchemaNode, "error-message"); + final DataSchemaNode errMsgSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode); + checkState(errMsgSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errMsgSchemaNode) + .withValue(error.getErrorMessage()).build()); + + if (error.getErrorInfo() != null) { + // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the + // intention is for implementors to define their own data content so we'll just treat it as a leaf + // with string data. + errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME, + error.getErrorInfo())); + } + + // TODO : find how could we add possible "error-path" + + return errNodeValues.build(); + } + + private static String toJsonResponseBody(final NormalizedNodeContext errorsNode) { + final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + NormalizedNode data = errorsNode.getData(); + final InstanceIdentifierContext context = errorsNode.getInstanceIdentifierContext(); + final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode(); + + final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8); + if (data == null) { + throw new RestconfDocumentedException(Response.Status.NOT_FOUND); + } + + final boolean isDataRoot; + final var stack = context.inference().toSchemaInferenceStack(); + if (stack.isEmpty()) { + isDataRoot = true; + } else { + isDataRoot = false; + stack.exit(); + // FIXME: Add proper handling of reading root. + } + + XMLNamespace initialNs = null; + if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) { + initialNs = schema.getQName().getNamespace(); + } + + final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter); + final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter( + JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), + stack.toInference(), initialNs, jsonWriter); + + // We create a delegating writer to special-case error-info as error-info is defined as an empty + // container in the restconf yang schema but we create a leaf node so we can output it. The delegate + // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but + // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior + // for error-info. + final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() { + private boolean inOurLeaf; + + @Override + protected NormalizedNodeStreamWriter delegate() { + return jsonStreamWriter; + } + + @Override + public void startLeafNode(final NodeIdentifier name) throws IOException { + if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) { + inOurLeaf = true; + jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName()); + } else { + super.startLeafNode(name); + } + } + + @Override + public void scalarValue(final Object value) throws IOException { + if (inOurLeaf) { + jsonWriter.value(value.toString()); + } else { + super.scalarValue(value); + } + } + + @Override + public void endNode() throws IOException { + if (inOurLeaf) { + inOurLeaf = false; + } else { + super.endNode(); + } + } + }; + + final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter); + try { + if (isDataRoot) { + writeDataRoot(outputWriter,nnWriter,(ContainerNode) data); + } else { + if (data instanceof MapEntryNode) { + data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType()) + .withChild((MapEntryNode) data) + .build(); + } + nnWriter.write(data); + } + nnWriter.flush(); + outputWriter.flush(); + } catch (final IOException e) { + LOG.warn("Error writing error response body", e); + } + + try { + streamWriter.close(); + } catch (IOException e) { + LOG.warn("Failed to close stream writer", e); + } + + return outStream.toString(StandardCharsets.UTF_8); + } + + private static String toXMLResponseBody(final NormalizedNodeContext errorsNode) { + final InstanceIdentifierContext pathContext = errorsNode.getInstanceIdentifierContext(); + final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + + final XMLStreamWriter xmlWriter; + try { + xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name()); + } catch (final XMLStreamException | FactoryConfigurationError e) { + throw new IllegalStateException(e); + } + NormalizedNode data = errorsNode.getData(); + + final boolean isDataRoot; + final var stack = pathContext.inference().toSchemaInferenceStack(); + if (stack.isEmpty()) { + isDataRoot = true; + } else { + isDataRoot = false; + stack.exit(); + } + + final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, + stack.toInference()); + + // We create a delegating writer to special-case error-info as error-info is defined as an empty + // container in the restconf yang schema but we create a leaf node so we can output it. The delegate + // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but + // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior + // for error-info. + final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() { + private boolean inOurLeaf; + + @Override + protected NormalizedNodeStreamWriter delegate() { + return xmlStreamWriter; + } + + @Override + public void startLeafNode(final NodeIdentifier name) throws IOException { + if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) { + String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString(); + try { + xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, + Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns); + } catch (XMLStreamException e) { + throw new IOException("Error writing error-info", e); + } + inOurLeaf = true; + } else { + super.startLeafNode(name); + } + } + + @Override + public void scalarValue(final Object value) throws IOException { + if (inOurLeaf) { + try { + xmlWriter.writeCharacters(value.toString()); + } catch (XMLStreamException e) { + throw new IOException("Error writing error-info", e); + } + } else { + super.scalarValue(value); + } + } + + @Override + public void endNode() throws IOException { + if (inOurLeaf) { + try { + xmlWriter.writeEndElement(); + } catch (XMLStreamException e) { + throw new IOException("Error writing error-info", e); + } + inOurLeaf = false; + } else { + super.endNode(); + } + } + }; + + final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter); + try { + if (isDataRoot) { + writeRootElement(xmlWriter, nnWriter, (ContainerNode) data); + } else { + if (data instanceof MapEntryNode) { + // Restconf allows returning one list item. We need to wrap it + // in map node in order to serialize it properly + data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType()) + .addChild((MapEntryNode) data) + .build(); + } + nnWriter.write(data); + nnWriter.flush(); + } + } catch (final IOException e) { + LOG.warn("Error writing error response body.", e); + } + + return outStream.toString(StandardCharsets.UTF_8); + } + + private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter, + final ContainerNode data) throws IOException { + final QName name = SchemaContext.NAME; + try { + xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName()); + for (final DataContainerChild child : data.body()) { + nnWriter.write(child); + } + nnWriter.flush(); + xmlWriter.writeEndElement(); + xmlWriter.flush(); + } catch (final XMLStreamException e) { + throw new IOException("Failed to write elements", e); + } + } + + private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter, + final ContainerNode data) throws IOException { + for (final DataContainerChild child : data.body()) { + nnWriter.write(child); + nnWriter.flush(); + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/StringModuleInstanceIdentifierCodec.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/StringModuleInstanceIdentifierCodec.java new file mode 100644 index 0000000..b61ed58 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/StringModuleInstanceIdentifierCodec.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static java.util.Objects.requireNonNull; + +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.data.util.AbstractModuleStringInstanceIdentifierCodec; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +/** + * Codec for module instance identifiers. + * + * @deprecated This class will be replaced by StringModuleInstanceIdentifierCodec from restconf-nb-rfc8040 + */ +@Deprecated +public final class StringModuleInstanceIdentifierCodec extends AbstractModuleStringInstanceIdentifierCodec { + + private final DataSchemaContextTree dataContextTree; + private final SchemaContext context; + private final String defaultPrefix; + + public StringModuleInstanceIdentifierCodec(final EffectiveModelContext context) { + this.context = requireNonNull(context); + this.dataContextTree = DataSchemaContextTree.from(context); + this.defaultPrefix = ""; + } + + StringModuleInstanceIdentifierCodec(final EffectiveModelContext context, final @NonNull String defaultPrefix) { + this.context = requireNonNull(context); + this.dataContextTree = DataSchemaContextTree.from(context); + this.defaultPrefix = defaultPrefix; + } + + @Override + protected Module moduleForPrefix(final String prefix) { + if (prefix.isEmpty() && !this.defaultPrefix.isEmpty()) { + return this.context.findModules(this.defaultPrefix).stream().findFirst().orElse(null); + } else { + return this.context.findModules(prefix).stream().findFirst().orElse(null); + } + } + + @Override + protected DataSchemaContextTree getDataContextTree() { + return this.dataContextTree; + } + + @Override + protected String prefixForNamespace(final XMLNamespace namespace) { + return this.context.findModules(namespace).stream().findFirst().map(Module::getName).orElse(null); + } +}
\ No newline at end of file diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/UnsupportedFormatException.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/UnsupportedFormatException.java new file mode 100644 index 0000000..9e21659 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/UnsupportedFormatException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +public class UnsupportedFormatException extends Exception { + + private static final long serialVersionUID = -1741388894406313402L; + + public UnsupportedFormatException() { + } + + public UnsupportedFormatException(String message, Throwable cause) { + super(message, cause); + } + + public UnsupportedFormatException(String message) { + super(message); + } + + public UnsupportedFormatException(Throwable cause) { + super(cause); + } + +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/WriterParameters.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/WriterParameters.java new file mode 100644 index 0000000..1ad6985 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/WriterParameters.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +@Deprecated(forRemoval = true, since = "2.0.6") +public final class WriterParameters { + static final WriterParameters EMPTY = new WriterParametersBuilder().build(); + + private final Integer depth; + private final boolean prettyPrint; + + private WriterParameters(final WriterParametersBuilder builder) { + depth = builder.depth; + prettyPrint = builder.prettyPrint; + } + + public Integer getDepth() { + return depth; + } + + public boolean isPrettyPrint() { + return prettyPrint; + } + + @Deprecated(forRemoval = true, since = "2.0.6") + public static final class WriterParametersBuilder { + private Integer depth; + private boolean prettyPrint; + + public WriterParametersBuilder setDepth(final int depth) { + this.depth = depth; + return this; + } + + public WriterParametersBuilder setPrettyPrint(final boolean prettyPrint) { + this.prettyPrint = prettyPrint; + return this; + } + + public WriterParameters build() { + return new WriterParameters(this); + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java new file mode 100644 index 0000000..84a7978 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static com.google.common.base.Preconditions.checkState; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.dom.DOMSource; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.util.RestUtil; +import org.opendaylight.yangtools.util.xml.UntrustedXML; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.ErrorType; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree; +import org.opendaylight.yangtools.yang.model.api.ContainerLike; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; +import org.opendaylight.yangtools.yang.model.api.stmt.ListEffectiveStatement; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +@Provider +@Consumes({ + Draft02.MediaTypes.DATA + RestconfService.XML, + Draft02.MediaTypes.OPERATION + RestconfService.XML, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML +}) +public class XmlNormalizedNodeBodyReader extends AbstractIdentifierAwareJaxRsProvider + implements MessageBodyReader<NormalizedNodeContext> { + + private static final Logger LOG = LoggerFactory.getLogger(XmlNormalizedNodeBodyReader.class); + + public XmlNormalizedNodeBodyReader(final ControllerContext controllerContext) { + super(controllerContext); + } + + @Override + public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType) { + return true; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public NormalizedNodeContext readFrom(final Class<NormalizedNodeContext> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws + WebApplicationException { + try { + return readFrom(entityStream); + } catch (final RestconfDocumentedException e) { + throw e; + } catch (final Exception e) { + LOG.debug("Error parsing xml input", e); + RestconfDocumentedException.throwIfYangError(e); + throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, e); + } + } + + private NormalizedNodeContext readFrom(final InputStream entityStream) throws IOException, SAXException, + XMLStreamException, ParserConfigurationException, URISyntaxException { + final InstanceIdentifierContext path = getInstanceIdentifierContext(); + final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream); + if (nonEmptyInputStreamOptional.isEmpty()) { + // represent empty nopayload input + return new NormalizedNodeContext(path, null); + } + + final Document doc = UntrustedXML.newDocumentBuilder().parse(nonEmptyInputStreamOptional.get()); + return parse(path, doc); + } + + private NormalizedNodeContext parse(final InstanceIdentifierContext pathContext,final Document doc) + throws XMLStreamException, IOException, SAXException, URISyntaxException { + final SchemaNode schemaNodeContext = pathContext.getSchemaNode(); + DataSchemaNode schemaNode; + final List<PathArgument> iiToDataList = new ArrayList<>(); + Inference inference; + if (schemaNodeContext instanceof RpcDefinition) { + schemaNode = ((RpcDefinition) schemaNodeContext).getInput(); + inference = pathContext.inference(); + } else if (schemaNodeContext instanceof DataSchemaNode) { + schemaNode = (DataSchemaNode) schemaNodeContext; + + final String docRootElm = doc.getDocumentElement().getLocalName(); + final XMLNamespace docRootNamespace = XMLNamespace.of(doc.getDocumentElement().getNamespaceURI()); + + if (isPost()) { + final var context = pathContext.getSchemaContext(); + final var it = context.findModuleStatements(docRootNamespace).iterator(); + checkState(it.hasNext(), "Failed to find module for %s", docRootNamespace); + final var qname = QName.create(it.next().localQNameModule(), docRootElm); + + final var nodeAndStack = DataSchemaContextTree.from(context) + .enterPath(pathContext.getInstanceIdentifier()).orElseThrow(); + + final var stack = nodeAndStack.stack(); + var current = nodeAndStack.node(); + do { + final var next = current.enterChild(stack, qname); + checkState(next != null, "Child \"%s\" was not found in parent schema node \"%s\"", qname, + schemaNode); + iiToDataList.add(next.getIdentifier()); + schemaNode = next.getDataSchemaNode(); + current = next; + } while (current.isMixin()); + + // We need to unwind the last identifier if it a NodeIdentifierWithPredicates, as it does not have + // any predicates at all. The real identifier is then added below + if (stack.currentStatement() instanceof ListEffectiveStatement) { + iiToDataList.remove(iiToDataList.size() - 1); + } + + inference = stack.toInference(); + + } else { + // PUT + final QName scQName = schemaNode.getQName(); + checkState(docRootElm.equals(scQName.getLocalName()) && docRootNamespace.equals(scQName.getNamespace()), + "Not correct message root element \"%s\", should be \"%s\"", docRootElm, scQName); + inference = pathContext.inference(); + } + } else { + throw new IllegalStateException("Unknown SchemaNode"); + } + + + NormalizedNode parsed; + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + + if (schemaNode instanceof ContainerLike || schemaNode instanceof ListSchemaNode + || schemaNode instanceof LeafSchemaNode) { + final XmlParserStream xmlParser = XmlParserStream.create(writer, inference); + xmlParser.traverse(new DOMSource(doc.getDocumentElement())); + parsed = resultHolder.getResult(); + + // When parsing an XML source with a list root node + // the new XML parser always returns a MapNode with one MapEntryNode inside. + // However, the old XML parser returned a MapEntryNode directly in this place. + // Therefore we now have to extract the MapEntryNode from the parsed MapNode. + if (parsed instanceof MapNode) { + final MapNode mapNode = (MapNode) parsed; + // extracting the MapEntryNode + parsed = mapNode.body().iterator().next(); + } + + if (schemaNode instanceof ListSchemaNode && isPost()) { + iiToDataList.add(parsed.getIdentifier()); + } + } else { + LOG.warn("Unknown schema node extension {} was not parsed", schemaNode.getClass()); + parsed = null; + } + + return new NormalizedNodeContext(pathContext.withConcatenatedArgs(iiToDataList), parsed); + } +} + diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java new file mode 100644 index 0000000..9cc4766 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import static com.google.common.base.Verify.verify; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.dom.DOMSource; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchEditOperation; +import org.opendaylight.restconf.common.patch.PatchEntity; +import org.opendaylight.restconf.common.util.RestUtil; +import org.opendaylight.yangtools.util.xml.UntrustedXML; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.ErrorType; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.Revision; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; +import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Yang PATCH Reader for XML. + * + * @deprecated This class will be replaced by XmlToPatchBodyReader from restconf-nb-rfc8040 + */ +@Deprecated +@Provider +@Consumes({Draft02.MediaTypes.PATCH + RestconfService.XML}) +public class XmlToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider implements + MessageBodyReader<PatchContext> { + + private static final Logger LOG = LoggerFactory.getLogger(XmlToPatchBodyReader.class); + + public XmlToPatchBodyReader(final ControllerContext controllerContext) { + super(controllerContext); + } + + @Override + public boolean isReadable(final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return true; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public PatchContext readFrom(final Class<PatchContext> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) + throws WebApplicationException { + + try { + final InstanceIdentifierContext path = getInstanceIdentifierContext(); + final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream); + if (nonEmptyInputStreamOptional.isEmpty()) { + // represent empty nopayload input + return new PatchContext(path, null, null); + } + + final Document doc = UntrustedXML.newDocumentBuilder().parse(nonEmptyInputStreamOptional.get()); + return parse(path, doc); + } catch (final RestconfDocumentedException e) { + throw e; + } catch (final Exception e) { + LOG.debug("Error parsing xml input", e); + RestconfDocumentedException.throwIfYangError(e); + throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, e); + } + } + + private static PatchContext parse(final InstanceIdentifierContext pathContext, final Document doc) + throws XMLStreamException, IOException, ParserConfigurationException, SAXException, URISyntaxException { + final List<PatchEntity> resultCollection = new ArrayList<>(); + final String patchId = doc.getElementsByTagName("patch-id").item(0).getFirstChild().getNodeValue(); + final NodeList editNodes = doc.getElementsByTagName("edit"); + + for (int i = 0; i < editNodes.getLength(); i++) { + DataSchemaNode schemaNode = (DataSchemaNode) pathContext.getSchemaNode(); + final Element element = (Element) editNodes.item(i); + final String operation = element.getElementsByTagName("operation").item(0).getFirstChild().getNodeValue(); + final PatchEditOperation oper = PatchEditOperation.valueOf(operation.toUpperCase(Locale.ROOT)); + + final String editId = element.getElementsByTagName("edit-id").item(0).getFirstChild().getNodeValue(); + final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue(); + final List<Element> values = readValueNodes(element, oper); + final Element firstValueElement = values != null ? values.get(0) : null; + + // get namespace according to schema node from path context or value + final String namespace = firstValueElement == null + ? schemaNode.getQName().getNamespace().toString() : firstValueElement.getNamespaceURI(); + + // find module according to namespace + final Module module = pathContext.getSchemaContext().findModules(XMLNamespace.of(namespace)).iterator() + .next(); + + // initialize codec + set default prefix derived from module name + final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec( + pathContext.getSchemaContext(), module.getName()); + + // find complete path to target and target schema node + // target can be also empty (only slash) + YangInstanceIdentifier targetII; + final SchemaNode targetNode; + final Inference inference; + if (target.equals("/")) { + targetII = pathContext.getInstanceIdentifier(); + targetNode = pathContext.getSchemaContext(); + inference = pathContext.inference(); + } else { + targetII = codec.deserialize(codec.serialize(pathContext.getInstanceIdentifier()) + .concat(prepareNonCondXpath(schemaNode, target.replaceFirst("/", ""), firstValueElement, + namespace, + module.getQNameModule().getRevision().map(Revision::toString).orElse(null)))); + // move schema node + final var result = codec.getDataContextTree().enterPath(targetII).orElseThrow(); + schemaNode = result.node().getDataSchemaNode(); + + final var stack = result.stack(); + inference = stack.toInference(); + + stack.exit(); + final EffectiveStatement<?, ?> parentStmt = stack.currentStatement(); + verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt); + targetNode = (SchemaNode) parentStmt; + } + + if (targetNode == null) { + LOG.debug("Target node {} not found in path {} ", target, pathContext.getSchemaNode()); + throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE); + } + + if (oper.isWithValue()) { + final NormalizedNode parsed; + if (schemaNode instanceof ContainerSchemaNode || schemaNode instanceof ListSchemaNode) { + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + final XmlParserStream xmlParser = XmlParserStream.create(writer, inference); + xmlParser.traverse(new DOMSource(firstValueElement)); + parsed = resultHolder.getResult(); + } else { + parsed = null; + } + + // for lists allow to manipulate with list items through their parent + if (targetII.getLastPathArgument() instanceof NodeIdentifierWithPredicates) { + targetII = targetII.getParent(); + } + + resultCollection.add(new PatchEntity(editId, oper, targetII, parsed)); + } else { + resultCollection.add(new PatchEntity(editId, oper, targetII)); + } + } + + return new PatchContext(pathContext, ImmutableList.copyOf(resultCollection), patchId); + } + + /** + * Read value nodes. + * + * @param element Element of current edit operation + * @param operation Name of current operation + * @return List of value elements + */ + private static List<Element> readValueNodes(final @NonNull Element element, + final @NonNull PatchEditOperation operation) { + final Node valueNode = element.getElementsByTagName("value").item(0); + + if (operation.isWithValue() && valueNode == null) { + throw new RestconfDocumentedException("Error parsing input", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + if (!operation.isWithValue() && valueNode != null) { + throw new RestconfDocumentedException("Error parsing input", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + if (valueNode == null) { + return null; + } + + final List<Element> result = new ArrayList<>(); + final NodeList childNodes = valueNode.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (childNodes.item(i) instanceof Element) { + result.add((Element) childNodes.item(i)); + } + } + + return result; + } + + /** + * Prepare non-conditional XPath suitable for deserialization with {@link StringModuleInstanceIdentifierCodec}. + * + * @param schemaNode Top schema node + * @param target Edit operation target + * @param value Element with value + * @param namespace Module namespace + * @param revision Module revision + * @return Non-conditional XPath + */ + private static String prepareNonCondXpath(final @NonNull DataSchemaNode schemaNode, final @NonNull String target, + final @NonNull Element value, final @NonNull String namespace, final @NonNull String revision) { + final Iterator<String> args = Splitter.on("/").split(target.substring(target.indexOf(':') + 1)).iterator(); + + final StringBuilder nonCondXpath = new StringBuilder(); + SchemaNode childNode = schemaNode; + + while (args.hasNext()) { + final String s = args.next(); + nonCondXpath.append('/').append(s); + childNode = ((DataNodeContainer) childNode).getDataChildByName(QName.create(namespace, revision, s)); + + if (childNode instanceof ListSchemaNode && args.hasNext()) { + appendKeys(nonCondXpath, ((ListSchemaNode) childNode).getKeyDefinition().iterator(), args); + } + } + + if (childNode instanceof ListSchemaNode && value != null) { + final Iterator<String> keyValues = readKeyValues(value, + ((ListSchemaNode) childNode).getKeyDefinition().iterator()); + appendKeys(nonCondXpath, ((ListSchemaNode) childNode).getKeyDefinition().iterator(), keyValues); + } + + return nonCondXpath.toString(); + } + + /** + * Read value for every list key. + * + * @param value Value element + * @param keys Iterator of list keys names + * @return Iterator of list keys values + */ + private static Iterator<String> readKeyValues(final @NonNull Element value, final @NonNull Iterator<QName> keys) { + final List<String> result = new ArrayList<>(); + + while (keys.hasNext()) { + result.add(value.getElementsByTagName(keys.next().getLocalName()).item(0).getFirstChild().getNodeValue()); + } + + return result.iterator(); + } + + /** + * Append key name - key value pairs for every list key to {@code nonCondXpath}. + * + * @param nonCondXpath Builder for creating non-conditional XPath + * @param keyNames Iterator of list keys names + * @param keyValues Iterator of list keys values + */ + private static void appendKeys(final @NonNull StringBuilder nonCondXpath, final @NonNull Iterator<QName> keyNames, + final @NonNull Iterator<String> keyValues) { + while (keyNames.hasNext()) { + nonCondXpath.append("[").append(keyNames.next().getLocalName()).append("='").append(keyValues.next()) + .append("']"); + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/package-info.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/package-info.java new file mode 100644 index 0000000..07eb8e2 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/package-info.java @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl;
\ No newline at end of file |