import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.utils.parser.ParserIdentifier;
+/**
+ * JaxRd identifier aware provider.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
public abstract class AbstractIdentifierAwareJaxRsProvider<T> implements MessageBodyReader<T> {
@Context
/**
* Common superclass for readers producing {@link NormalizedNodeContext}.
*
+ * @deprecated move to splitted module restconf-nb-rfc8040
* @author Robert Varga
*/
+@Deprecated
@Beta
public abstract class AbstractNormalizedNodeBodyReader
extends AbstractIdentifierAwareJaxRsProvider<NormalizedNodeContext> {
/**
* Common superclass for readers producing {@link PatchContext}.
*
+ * @deprecated move to splitted module restconf-nb-rfc8040
* @author Robert Varga
*/
+@Deprecated
abstract class AbstractToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider<PatchContext> {
@Override
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Reader of Json to NormalizedNode.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
@Provider
@Consumes({ Rfc8040.MediaTypes.DATA + RestconfConstants.JSON, MediaType.APPLICATION_JSON })
public class JsonNormalizedNodeBodyReader extends AbstractNormalizedNodeBodyReader {
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Patch reader.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
@Provider
@Consumes({Rfc8040.MediaTypes.PATCH + RestconfConstants.JSON})
public class JsonToPatchBodyReader extends AbstractToPatchBodyReader {
import org.opendaylight.yangtools.yang.model.api.SchemaNode;
import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+/**
+ * Writer NormalizedNode to Json.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
@Provider
@Produces({ Rfc8040.MediaTypes.DATA + RestconfConstants.JSON, MediaType.APPLICATION_JSON })
public class NormalizedNodeJsonBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+/**
+ * Writer of NormalizedNode to XML.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
@Provider
@Produces({ Rfc8040.MediaTypes.DATA + RestconfConstants.XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public class NormalizedNodeXmlBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
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.
+ * 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 move to splitted module restconf-nb-rfc8040
*/
+@Deprecated
@Beta
public class ParameterAwareNormalizedNodeWriter implements RestconfNormalizedNodeWriter {
private static final QName ROOT_DATA_QNAME = QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data");
import org.opendaylight.yangtools.yang.model.api.Module;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+/**
+ * Module instance identifier codec.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
public final class StringModuleInstanceIdentifierCodec extends AbstractModuleStringInstanceIdentifierCodec {
private final DataSchemaContextTree dataContextTree;
private final SchemaContext context;
private final String defaultPrefix;
- public StringModuleInstanceIdentifierCodec(SchemaContext context) {
+ public StringModuleInstanceIdentifierCodec(final SchemaContext context) {
this.context = Preconditions.checkNotNull(context);
this.dataContextTree = DataSchemaContextTree.from(context);
this.defaultPrefix = "";
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
+/**
+ * Reader of XML to NormalizedNode.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
@Provider
@Consumes({ Rfc8040.MediaTypes.DATA + RestconfConstants.XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public class XmlNormalizedNodeBodyReader extends AbstractNormalizedNodeBodyReader {
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
+/**
+ * Reader of XML to Patch.
+ *
+ * @deprecated move to splitted module restconf-nb-rfc8040
+ */
+@Deprecated
@Provider
@Consumes({Rfc8040.MediaTypes.PATCH + RestconfConstants.XML})
public class XmlToPatchBodyReader extends AbstractToPatchBodyReader {
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-test-util</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-model-export</artifactId>
+ </dependency>
<dependency>
<groupId>org.opendaylight.mdsal.model</groupId>
<version>3.0</version>
</dependency>
+ <dependency>
+ <groupId>net.java.dev.stax-utils</groupId>
+ <artifactId>stax-utils</artifactId>
+ </dependency>
+
<!-- Testing Dependencies -->
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
private final SchemaContext context;
private final String defaultPrefix;
- public StringModuleInstanceIdentifierCodec(SchemaContext context) {
+ public StringModuleInstanceIdentifierCodec(final SchemaContext context) {
this.context = Preconditions.checkNotNull(context);
this.dataContextTree = DataSchemaContextTree.from(context);
this.defaultPrefix = "";
}
- StringModuleInstanceIdentifierCodec(final SchemaContext context, @Nonnull final String defaultPrefix) {
+ public StringModuleInstanceIdentifierCodec(final SchemaContext context, @Nonnull final String defaultPrefix) {
this.context = Preconditions.checkNotNull(context);
this.dataContextTree = DataSchemaContextTree.from(context);
this.defaultPrefix = defaultPrefix;
@Nonnull
@Override
- protected DataSchemaContextTree getDataContextTree() {
+ public DataSchemaContextTree getDataContextTree() {
return this.dataContextTree;
}
*/
package org.opendaylight.restconf.nb.rfc8040.handlers;
+import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
public class DOMMountPointServiceHandler implements Handler<DOMMountPointService> {
private final DOMMountPointService domMountPointService;
+ private static DOMMountPointService actualDomMountPointService;
/**
* Prepare mount point service for Restconf services.
public DOMMountPointServiceHandler(final DOMMountPointService domMountPointService) {
Preconditions.checkNotNull(domMountPointService);
this.domMountPointService = domMountPointService;
+ actualDomMountPointService = domMountPointService;
}
@Override
return this.domMountPointService;
}
+ public static Optional<DOMMountPointService> getActualMountPointService() {
+ return Optional.fromNullable(actualDomMountPointService);
+ }
+
}
private final TransactionChainHandler transactionChainHandler;
private SchemaContext context;
+ private static SchemaContext actualSchemaContext;
private int moduleSetId;
public SchemaContextHandler(final TransactionChainHandler transactionChainHandler) {
this.transactionChainHandler = transactionChainHandler;
this.moduleSetId = 0;
+ actualSchemaContext = null;
}
@Override
Preconditions.checkNotNull(context);
this.context = null;
this.context = context;
+
+ actualSchemaContext = context;
+
this.moduleSetId++;
final Module ietfYangLibraryModule =
context.findModuleByNamespaceAndRevision(IetfYangLibrary.URI_MODULE, IetfYangLibrary.DATE);
return this.context;
}
+ public static SchemaContext getActualSchemaContext() {
+ return actualSchemaContext;
+ }
+
private void putData(
final NormalizedNode<NodeIdentifier, Collection<DataContainerChild<? extends PathArgument, ?>>> normNode) {
final DOMDataWriteTransaction wTx = this.transactionChainHandler.get().newWriteOnlyTransaction();
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
+
+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.util.ArrayList;
+import java.util.List;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.Provider;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.spi.AbstractNormalizedNodeBodyReader;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+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.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.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Provider
+@Consumes({ Rfc8040.MediaTypes.DATA + RestconfConstants.JSON, MediaType.APPLICATION_JSON })
+public class JsonNormalizedNodeBodyReader extends AbstractNormalizedNodeBodyReader {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JsonNormalizedNodeBodyReader.class);
+
+ @SuppressWarnings("checkstyle:IllegalCatch")
+ @Override
+ protected NormalizedNodeContext readBody(final InstanceIdentifierContext<?> path, final InputStream entityStream)
+ throws IOException, WebApplicationException {
+ try {
+ return readFrom(path, entityStream, isPost());
+ } catch (final Exception e) {
+ propagateExceptionAs(e);
+ return null;
+ }
+ }
+
+ public static NormalizedNodeContext readFrom(
+ final InstanceIdentifierContext<?> path, final InputStream entityStream, final boolean isPost)
+ throws IOException {
+ final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+ final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
+
+ final SchemaNode parentSchema;
+ if (isPost) {
+ parentSchema = path.getSchemaNode();
+ } else if (path.getSchemaNode() instanceof SchemaContext) {
+ parentSchema = path.getSchemaContext();
+ } else {
+ if (SchemaPath.ROOT.equals(path.getSchemaNode().getPath().getParent())) {
+ parentSchema = path.getSchemaContext();
+ } else {
+ parentSchema = SchemaContextUtil
+ .findDataSchemaNode(path.getSchemaContext(), path.getSchemaNode().getPath().getParent());
+ }
+ }
+
+ final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(), parentSchema);
+ final JsonReader reader = new JsonReader(new InputStreamReader(entityStream));
+ jsonParser.parse(reader);
+
+ NormalizedNode<?, ?> result = resultHolder.getResult();
+ final List<YangInstanceIdentifier.PathArgument> iiToDataList = new ArrayList<>();
+ InstanceIdentifierContext<? extends SchemaNode> newIIContext;
+
+ while (result instanceof AugmentationNode || result instanceof ChoiceNode) {
+ final Object childNode = ((DataContainerNode<?>) result).getValue().iterator().next();
+ if (isPost) {
+ iiToDataList.add(result.getIdentifier());
+ }
+ result = (NormalizedNode<?, ?>) childNode;
+ }
+
+ if (isPost) {
+ if (result instanceof MapEntryNode) {
+ iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(result.getNodeType()));
+ iiToDataList.add(result.getIdentifier());
+ } else {
+ iiToDataList.add(result.getIdentifier());
+ }
+ } else {
+ if (result instanceof MapNode) {
+ result = Iterables.getOnlyElement(((MapNode) result).getValue());
+ }
+ }
+
+ final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat(
+ path.getInstanceIdentifier().getPathArguments(), iiToDataList));
+
+ newIIContext = new InstanceIdentifierContext<>(fullIIToData, path.getSchemaNode(), path.getMountPoint(),
+ path.getSchemaContext());
+
+ return new NormalizedNodeContext(newIIContext, result);
+ }
+
+ private static void propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
+ if (exception instanceof RestconfDocumentedException) {
+ throw (RestconfDocumentedException)exception;
+ }
+
+ if (exception instanceof ResultAlreadySetException) {
+ LOG.debug("Error parsing json input:", exception);
+
+ throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. "
+ + "Are you creating multiple resources/subresources in POST request?", exception);
+ }
+
+ LOG.debug("Error parsing json input", exception);
+
+ throw new RestconfDocumentedException("Error parsing input: " + exception.getMessage(), ErrorType.PROTOCOL,
+ ErrorTag.MALFORMED_MESSAGE, exception);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
+
+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.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+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.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+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.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
+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.api.SchemaPath;
+
+@Provider
+@Produces({ Rfc8040.MediaTypes.DATA + RestconfConstants.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 {
+ final NormalizedNode<?, ?> data = context.getData();
+ if (data == null) {
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ final InstanceIdentifierContext<SchemaNode> identifierCtx =
+ (InstanceIdentifierContext<SchemaNode>) context.getInstanceIdentifierContext();
+ final SchemaPath path = identifierCtx.getSchemaNode().getPath();
+ final JsonWriter jsonWriter = createJsonWriter(entityStream,
+ context.getWriterParameters().isPrettyPrint());
+
+ jsonWriter.beginObject();
+ writeNormalizedNode(jsonWriter, path, identifierCtx, data,
+ context.getWriterParameters().getDepth(), context.getWriterParameters().getFields());
+ jsonWriter.endObject();
+ jsonWriter.flush();
+ }
+
+ private static void writeNormalizedNode(final JsonWriter jsonWriter,
+ final SchemaPath path, final InstanceIdentifierContext<SchemaNode> context, final NormalizedNode<?, ?> data,
+ final Integer depth, final List<Set<QName>> fields) throws IOException {
+ final RestconfNormalizedNodeWriter nnWriter;
+
+ if (context.getSchemaNode() instanceof RpcDefinition) {
+ /*
+ * RpcDefinition is not supported as initial codec in JSONStreamWriter,
+ * so we need to emit initial output declaration..
+ */
+ nnWriter = createNormalizedNodeWriter(
+ context,
+ ((RpcDefinition) context.getSchemaNode()).getOutput().getPath(),
+ jsonWriter,
+ depth,
+ fields);
+ jsonWriter.name("output");
+ jsonWriter.beginObject();
+ writeChildren(nnWriter, (ContainerNode) data);
+ jsonWriter.endObject();
+ } else {
+ if (SchemaPath.ROOT.equals(path)) {
+ nnWriter = createNormalizedNodeWriter(context, path, jsonWriter, depth, fields);
+ } else {
+ nnWriter = createNormalizedNodeWriter(context, path.getParent(), jsonWriter, depth, fields);
+ }
+
+ if (data instanceof MapEntryNode) {
+ // Restconf allows returning one list item. We need to wrap it
+ // in map node in order to serialize it properly
+ nnWriter.write(
+ ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild((MapEntryNode) data).build());
+ } else {
+ nnWriter.write(data);
+ }
+ }
+
+ nnWriter.flush();
+ }
+
+ private static void writeChildren(final RestconfNormalizedNodeWriter nnWriter,
+ final ContainerNode data) throws IOException {
+ for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
+ nnWriter.write(child);
+ }
+ }
+
+ private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(
+ final InstanceIdentifierContext<SchemaNode> context, final SchemaPath path, final JsonWriter jsonWriter,
+ final Integer depth, final List<Set<QName>> fields) {
+
+ final SchemaNode schema = context.getSchemaNode();
+ final JSONCodecFactory codecs = getCodecFactory(context);
+
+ final URI initialNs;
+ if (schema instanceof DataSchemaNode
+ && !((DataSchemaNode)schema).isAugmenting()
+ && !(schema instanceof SchemaContext)) {
+ initialNs = schema.getQName().getNamespace();
+ } else if (schema instanceof RpcDefinition) {
+ initialNs = schema.getQName().getNamespace();
+ } else {
+ initialNs = null;
+ }
+ final NormalizedNodeStreamWriter streamWriter = JSONNormalizedNodeStreamWriter.createNestedWriter(
+ codecs, path, initialNs, jsonWriter);
+ return ParameterAwareNormalizedNodeWriter.forStreamWriter(streamWriter, depth, fields);
+ }
+
+ 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 JSONCodecFactory.getShared(context.getSchemaContext());
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
+
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+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.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+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.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+@Provider
+@Produces({ Rfc8040.MediaTypes.DATA + RestconfConstants.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 {
+ 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();
+ final SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
+
+ writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, context.getWriterParameters().getDepth(),
+ context.getWriterParameters().getFields());
+ }
+
+ private static void writeNormalizedNode(final XMLStreamWriter xmlWriter,
+ final SchemaPath path, final InstanceIdentifierContext<?> pathContext, final NormalizedNode<?, ?> data,
+ final Integer depth, final List<Set<QName>> fields) throws IOException {
+ final RestconfNormalizedNodeWriter nnWriter;
+ final SchemaContext schemaCtx = pathContext.getSchemaContext();
+
+ if (pathContext.getSchemaNode() instanceof RpcDefinition) {
+ /*
+ * RpcDefinition is not supported as initial codec in XMLStreamWriter,
+ * so we need to emit initial output declaration..
+ */
+ nnWriter = createNormalizedNodeWriter(
+ xmlWriter,
+ schemaCtx,
+ ((RpcDefinition) pathContext.getSchemaNode()).getOutput().getPath(),
+ depth,
+ fields);
+ writeElements(xmlWriter, nnWriter, (ContainerNode) data);
+ } else {
+ if (SchemaPath.ROOT.equals(path)) {
+ nnWriter = createNormalizedNodeWriter(xmlWriter, schemaCtx, path, depth, fields);
+ } else {
+ nnWriter = createNormalizedNodeWriter(xmlWriter, schemaCtx, path.getParent(), depth, fields);
+ }
+
+ if (data instanceof MapEntryNode) {
+ // Restconf allows returning one list item. We need to wrap it
+ // in map node in order to serialize it properly
+ nnWriter.write(ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build());
+ } else {
+ nnWriter.write(data);
+ }
+ }
+
+ nnWriter.flush();
+ }
+
+ private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final XMLStreamWriter xmlWriter,
+ final SchemaContext schemaContext, final SchemaPath schemaPath, final Integer depth,
+ final List<Set<QName>> fields) {
+ final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter
+ .create(xmlWriter, schemaContext, schemaPath);
+ return ParameterAwareNormalizedNodeWriter.forStreamWriter(xmlStreamWriter, depth, fields);
+ }
+
+ private static void writeElements(final XMLStreamWriter xmlWriter, final RestconfNormalizedNodeWriter nnWriter,
+ final ContainerNode data) throws IOException {
+ try {
+ final QName name = data.getNodeType();
+ xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
+ name.getLocalName(), name.getNamespace().toString());
+ xmlWriter.writeDefaultNamespace(name.getNamespace().toString());
+ for (final NormalizedNode<?,?> child : data.getValue()) {
+ nnWriter.write(child);
+ }
+ nnWriter.flush();
+ xmlWriter.writeEndElement();
+ xmlWriter.flush();
+ } catch (final XMLStreamException e) {
+ Throwables.propagate(e);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
+
+import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter.UNKNOWN_SIZE;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.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.OrderedLeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.OrderedMapNode;
+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.stream.NormalizedNodeStreamAttributeWriter;
+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.
+ */
+@Beta
+public class ParameterAwareNormalizedNodeWriter implements RestconfNormalizedNodeWriter {
+ private static final QName ROOT_DATA_QNAME = QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data");
+
+ private final NormalizedNodeStreamWriter writer;
+ private final Integer maxDepth;
+ protected final List<Set<QName>> fields;
+ protected int currentDepth = 0;
+
+ private ParameterAwareNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final Integer maxDepth,
+ final List<Set<QName>> fields) {
+ this.writer = Preconditions.checkNotNull(writer);
+ this.maxDepth = maxDepth;
+ this.fields = fields;
+ }
+
+ protected final NormalizedNodeStreamWriter getWriter() {
+ return writer;
+ }
+
+ /**
+ * Create a new writer backed by a {@link NormalizedNodeStreamWriter}.
+ *
+ * @param writer Back-end writer
+ * @param maxDepth Maximal depth to write
+ * @param fields Selected child nodes to write
+ * @return A new instance.
+ */
+ public static ParameterAwareNormalizedNodeWriter forStreamWriter(
+ final NormalizedNodeStreamWriter writer, final Integer maxDepth, final List<Set<QName>> fields) {
+ return forStreamWriter(writer, true, maxDepth, fields);
+ }
+
+ /**
+ * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. Unlike the simple
+ * {@link #forStreamWriter(NormalizedNodeStreamWriter, Integer, List)}
+ * 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.
+ * @param maxDepth Maximal depth to write
+ * @param fields Selected child nodes to write
+ * @return A new instance.
+ */
+ public static ParameterAwareNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
+ final boolean orderKeyLeaves,
+ final Integer maxDepth,
+ final List<Set<QName>> fields) {
+ return orderKeyLeaves ? new OrderedParameterAwareNormalizedNodeWriter(writer, maxDepth, fields)
+ : new ParameterAwareNormalizedNodeWriter(writer, maxDepth, fields);
+ }
+
+ /**
+ * Iterate over the provided {@link NormalizedNode} and emit write
+ * events to the encapsulated {@link NormalizedNodeStreamWriter}.
+ *
+ * @param node Node
+ * @return {@code ParameterAwareNormalizedNodeWriter}
+ * @throws IOException when thrown from the backing writer.
+ */
+ @Override
+ public final ParameterAwareNormalizedNodeWriter 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 (selectedByParameters(node, false)) {
+ final LeafSetEntryNode<?> nodeAsLeafList = (LeafSetEntryNode<?>) node;
+ if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+ ((NormalizedNodeStreamAttributeWriter) writer).leafSetEntryNode(nodeAsLeafList.getNodeType(),
+ nodeAsLeafList.getValue(), nodeAsLeafList.getAttributes());
+ } else {
+ writer.leafSetEntryNode(nodeAsLeafList.getNodeType(), nodeAsLeafList.getValue());
+ }
+ }
+ return true;
+ } else if (node instanceof LeafNode) {
+ final LeafNode<?> nodeAsLeaf = (LeafNode<?>)node;
+ if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+ ((NormalizedNodeStreamAttributeWriter) writer).leafNode(
+ nodeAsLeaf.getIdentifier(), nodeAsLeaf.getValue(), nodeAsLeaf.getAttributes());
+ } else {
+ writer.leafNode(nodeAsLeaf.getIdentifier(), nodeAsLeaf.getValue());
+ }
+ return true;
+ } else if (node instanceof AnyXmlNode) {
+ final AnyXmlNode anyXmlNode = (AnyXmlNode)node;
+ writer.anyxmlNode(anyXmlNode.getIdentifier(), anyXmlNode.getValue());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if node should be written according to parameters fields and depth.
+ * See <a href="https://tools.ietf.org/html/draft-ietf-netconf-restconf-18#page-49">Restconf draft</a>.
+ * @param node Node to be written
+ * @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
+ * @return {@code true} if node will be written, {@code false} otherwise
+ */
+ protected boolean selectedByParameters(final NormalizedNode<?, ?> node, final boolean mixinParent) {
+ // nodes to be written are not limited by fields, only by depth
+ if (fields == null) {
+ return maxDepth == null || currentDepth < maxDepth;
+ }
+
+ // children of mixin nodes are never selected in fields but must be written if they are first in selected target
+ if (mixinParent && currentDepth == 0) {
+ return true;
+ }
+
+ // always write augmentation nodes
+ if (node instanceof AugmentationNode) {
+ return true;
+ }
+
+ // write only selected nodes
+ if (currentDepth > 0 && currentDepth <= fields.size()) {
+ return fields.get(currentDepth - 1).contains(node.getNodeType());
+ }
+
+ // after this depth only depth parameter is used to determine when to write node
+ return maxDepth == null || currentDepth < maxDepth;
+ }
+
+ /**
+ * Emit events for all children and then emit an endNode() event.
+ *
+ * @param children Child iterable
+ * @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
+ * @return True
+ * @throws IOException when the writer reports it
+ */
+ protected final boolean writeChildren(final Iterable<? extends NormalizedNode<?, ?>> children,
+ final boolean mixinParent) throws IOException {
+ for (final NormalizedNode<?, ?> child : children) {
+ if (selectedByParameters(child, mixinParent)) {
+ write(child);
+ }
+ }
+ writer.endNode();
+ return true;
+ }
+
+ protected boolean writeMapEntryChildren(final MapEntryNode mapEntryNode) throws IOException {
+ if (selectedByParameters(mapEntryNode, false)) {
+ writeChildren(mapEntryNode.getValue(), false);
+ } else if (fields == null && maxDepth != null && currentDepth == maxDepth) {
+ writeOnlyKeys(mapEntryNode.getIdentifier().getKeyValues());
+ }
+ return true;
+ }
+
+ private void writeOnlyKeys(final Map<QName, Object> keyValues) throws IllegalArgumentException, IOException {
+ for (final Map.Entry<QName, Object> entry : keyValues.entrySet()) {
+ writer.leafNode(new NodeIdentifier(entry.getKey()), entry.getValue());
+ }
+ writer.endNode();
+ }
+
+ protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException {
+ if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+ ((NormalizedNodeStreamAttributeWriter) writer)
+ .startMapEntryNode(node.getIdentifier(), childSizeHint(node.getValue()), node.getAttributes());
+ } else {
+ writer.startMapEntryNode(node.getIdentifier(), childSizeHint(node.getValue()));
+ }
+ 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;
+ if (!n.getNodeType().equals(ROOT_DATA_QNAME)) {
+ if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+ ((NormalizedNodeStreamAttributeWriter) writer).startContainerNode(
+ n.getIdentifier(), childSizeHint(n.getValue()), n.getAttributes());
+ } else {
+ writer.startContainerNode(n.getIdentifier(), childSizeHint(n.getValue()));
+ }
+ currentDepth++;
+ processedAsCompositeNode = writeChildren(n.getValue(), false);
+ currentDepth--;
+ } else {
+ // write child nodes of data root container
+ for (final NormalizedNode<?, ?> child : n.getValue()) {
+ currentDepth++;
+ if (selectedByParameters(child, false)) {
+ write(child);
+ }
+ currentDepth--;
+ processedAsCompositeNode = true;
+ }
+ }
+ } 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.getValue()));
+ currentDepth++;
+ processedAsCompositeNode = writeChildren(n.getValue(), false);
+ currentDepth--;
+ } else if (node instanceof ChoiceNode) {
+ final ChoiceNode n = (ChoiceNode) node;
+ writer.startChoiceNode(n.getIdentifier(), childSizeHint(n.getValue()));
+ processedAsCompositeNode = writeChildren(n.getValue(), true);
+ } else if (node instanceof AugmentationNode) {
+ final AugmentationNode n = (AugmentationNode) node;
+ writer.startAugmentationNode(n.getIdentifier());
+ processedAsCompositeNode = writeChildren(n.getValue(), true);
+ } else if (node instanceof UnkeyedListNode) {
+ final UnkeyedListNode n = (UnkeyedListNode) node;
+ writer.startUnkeyedList(n.getIdentifier(), childSizeHint(n.getValue()));
+ processedAsCompositeNode = writeChildren(n.getValue(), false);
+ } else if (node instanceof OrderedMapNode) {
+ final OrderedMapNode n = (OrderedMapNode) node;
+ writer.startOrderedMapNode(n.getIdentifier(), childSizeHint(n.getValue()));
+ processedAsCompositeNode = writeChildren(n.getValue(), true);
+ } else if (node instanceof MapNode) {
+ final MapNode n = (MapNode) node;
+ writer.startMapNode(n.getIdentifier(), childSizeHint(n.getValue()));
+ processedAsCompositeNode = writeChildren(n.getValue(), true);
+ } else if (node instanceof LeafSetNode) {
+ final LeafSetNode<?> n = (LeafSetNode<?>) node;
+ if (node instanceof OrderedLeafSetNode) {
+ writer.startOrderedLeafSet(n.getIdentifier(), childSizeHint(n.getValue()));
+ } else {
+ writer.startLeafSet(n.getIdentifier(), childSizeHint(n.getValue()));
+ }
+ currentDepth++;
+ processedAsCompositeNode = writeChildren(n.getValue(), true);
+ currentDepth--;
+ }
+
+ return processedAsCompositeNode;
+ }
+
+ private static final class OrderedParameterAwareNormalizedNodeWriter extends ParameterAwareNormalizedNodeWriter {
+ private static final Logger LOG = LoggerFactory.getLogger(OrderedParameterAwareNormalizedNodeWriter.class);
+
+ OrderedParameterAwareNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final Integer maxDepth,
+ final List<Set<QName>> fields) {
+ super(writer, maxDepth, fields);
+ }
+
+ @Override
+ protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException {
+ final NormalizedNodeStreamWriter writer = getWriter();
+ if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+ ((NormalizedNodeStreamAttributeWriter) writer).startMapEntryNode(
+ node.getIdentifier(), childSizeHint(node.getValue()), node.getAttributes());
+ } else {
+ writer.startMapEntryNode(node.getIdentifier(), childSizeHint(node.getValue()));
+ }
+
+ final Set<QName> qnames = node.getIdentifier().getKeyValues().keySet();
+ // Write out all the key children
+ currentDepth++;
+ for (final QName qname : qnames) {
+ final Optional<? extends NormalizedNode<?, ?>> child = node.getChild(new NodeIdentifier(qname));
+ if (child.isPresent()) {
+ if (selectedByParameters(child.get(), false)) {
+ write(child.get());
+ }
+ } else {
+ LOG.info("No child for key element {} found", qname);
+ }
+ }
+ currentDepth--;
+
+ currentDepth++;
+ // Write all the rest
+ final boolean result =
+ writeChildren(Iterables.filter(node.getValue(), input -> {
+ if (input instanceof AugmentationNode) {
+ return true;
+ }
+ if (!qnames.contains(input.getNodeType())) {
+ return true;
+ }
+
+ LOG.debug("Skipping key child {}", input);
+ return false;
+ }), false);
+ currentDepth--;
+ return result;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.List;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+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.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.spi.AbstractNormalizedNodeBodyReader;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+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.impl.schema.SchemaUtils;
+import org.opendaylight.yangtools.yang.model.api.AugmentationSchema;
+import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
+import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
+import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
+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.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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+@Provider
+@Consumes({ Rfc8040.MediaTypes.DATA + RestconfConstants.XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+public class XmlNormalizedNodeBodyReader extends AbstractNormalizedNodeBodyReader {
+ private static final Logger LOG = LoggerFactory.getLogger(XmlNormalizedNodeBodyReader.class);
+
+ @SuppressWarnings("checkstyle:IllegalCatch")
+ @Override
+ protected NormalizedNodeContext readBody(final InstanceIdentifierContext<?> path, final InputStream entityStream)
+ throws IOException, WebApplicationException {
+ try {
+ final Document doc = UntrustedXML.newDocumentBuilder().parse(entityStream);
+ return parse(path,doc);
+ } catch (final RestconfDocumentedException e) {
+ throw e;
+ } catch (final Exception e) {
+ LOG.debug("Error parsing xml input", e);
+
+ throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
+ ErrorTag.MALFORMED_MESSAGE, e);
+ }
+ }
+
+ private NormalizedNodeContext parse(final InstanceIdentifierContext<?> pathContext, final Document doc)
+ throws XMLStreamException, IOException, ParserConfigurationException, SAXException, URISyntaxException {
+ final SchemaNode schemaNodeContext = pathContext.getSchemaNode();
+ DataSchemaNode schemaNode;
+ boolean isRpc = false;
+ if (schemaNodeContext instanceof RpcDefinition) {
+ schemaNode = ((RpcDefinition) schemaNodeContext).getInput();
+ isRpc = true;
+ } else if (schemaNodeContext instanceof DataSchemaNode) {
+ schemaNode = (DataSchemaNode) schemaNodeContext;
+ } else {
+ throw new IllegalStateException("Unknown SchemaNode");
+ }
+
+ final String docRootElm = doc.getDocumentElement().getLocalName();
+ final String docRootNamespace = doc.getDocumentElement().getNamespaceURI();
+ final List<YangInstanceIdentifier.PathArgument> iiToDataList = new ArrayList<>();
+
+ if (isPost() && !isRpc) {
+ final Deque<Object> foundSchemaNodes = findPathToSchemaNodeByName(schemaNode, docRootElm, docRootNamespace);
+ if (foundSchemaNodes.isEmpty()) {
+ throw new IllegalStateException(String.format("Child \"%s\" was not found in parent schema node \"%s\"",
+ docRootElm, schemaNode.getQName()));
+ }
+ while (!foundSchemaNodes.isEmpty()) {
+ final Object child = foundSchemaNodes.pop();
+ if (child instanceof AugmentationSchema) {
+ final AugmentationSchema augmentSchemaNode = (AugmentationSchema) child;
+ iiToDataList.add(SchemaUtils.getNodeIdentifierForAugmentation(augmentSchemaNode));
+ } else if (child instanceof DataSchemaNode) {
+ schemaNode = (DataSchemaNode) child;
+ iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(schemaNode.getQName()));
+ }
+ }
+ // PUT
+ } else if (!isRpc) {
+ final QName scQName = schemaNode.getQName();
+ Preconditions.checkState(
+ docRootElm.equals(scQName.getLocalName())
+ && docRootNamespace.equals(scQName.getNamespace().toASCIIString()),
+ String.format("Not correct message root element \"%s\", should be \"%s\"",
+ docRootElm, scQName));
+ }
+
+ NormalizedNode<?, ?> parsed;
+ final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+ final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
+
+ if (schemaNode instanceof ContainerSchemaNode || schemaNode instanceof ListSchemaNode
+ || schemaNode instanceof LeafSchemaNode) {
+ final XmlParserStream xmlParser = XmlParserStream.create(writer, pathContext.getSchemaContext(),
+ schemaNode);
+ 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.getValue().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;
+ }
+
+ final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat(
+ pathContext.getInstanceIdentifier().getPathArguments(), iiToDataList));
+
+ final InstanceIdentifierContext<? extends SchemaNode> outIIContext = new InstanceIdentifierContext<>(
+ fullIIToData, pathContext.getSchemaNode(), pathContext.getMountPoint(), pathContext.getSchemaContext());
+
+ return new NormalizedNodeContext(outIIContext, parsed);
+ }
+
+ private static Deque<Object> findPathToSchemaNodeByName(final DataSchemaNode schemaNode, final String elementName,
+ final String namespace) {
+ final Deque<Object> result = new ArrayDeque<>();
+ final ArrayList<ChoiceSchemaNode> choiceSchemaNodes = new ArrayList<>();
+ final Collection<DataSchemaNode> children = ((DataNodeContainer) schemaNode).getChildNodes();
+ for (final DataSchemaNode child : children) {
+ if (child instanceof ChoiceSchemaNode) {
+ choiceSchemaNodes.add((ChoiceSchemaNode) child);
+ } else if (child.getQName().getLocalName().equalsIgnoreCase(elementName)
+ && child.getQName().getNamespace().toString().equalsIgnoreCase(namespace)) {
+ // add child to result
+ result.push(child);
+
+ // find augmentation
+ if (child.isAugmenting()) {
+ final AugmentationSchema augment = findCorrespondingAugment(schemaNode, child);
+ if (augment != null) {
+ result.push(augment);
+ }
+ }
+
+ // return result
+ return result;
+ }
+ }
+
+ for (final ChoiceSchemaNode choiceNode : choiceSchemaNodes) {
+ for (final ChoiceCaseNode caseNode : choiceNode.getCases()) {
+ final Deque<Object> resultFromRecursion = findPathToSchemaNodeByName(caseNode, elementName, namespace);
+ if (!resultFromRecursion.isEmpty()) {
+ resultFromRecursion.push(choiceNode);
+ if (choiceNode.isAugmenting()) {
+ final AugmentationSchema augment = findCorrespondingAugment(schemaNode, choiceNode);
+ if (augment != null) {
+ resultFromRecursion.push(augment);
+ }
+ }
+ return resultFromRecursion;
+ }
+ }
+ }
+ return result;
+ }
+
+ private static AugmentationSchema findCorrespondingAugment(final DataSchemaNode parent,
+ final DataSchemaNode child) {
+ if (parent instanceof AugmentationTarget && !(parent instanceof ChoiceSchemaNode)) {
+ for (final AugmentationSchema augmentation : ((AugmentationTarget) parent).getAvailableAugmentations()) {
+ final DataSchemaNode childInAugmentation = augmentation.getDataChildByName(child.getQName());
+ if (childInAugmentation != null) {
+ return augmentation;
+ }
+ }
+ }
+ return null;
+ }
+}
+
--- /dev/null
+/*
+ * 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.restconf.nb.rfc8040.jersey.providers.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;
+}
--- /dev/null
+/*
+ * Copyright (c) 2017 Pantheon Technologies, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
+
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.patch.PatchContext;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.spi.AbstractIdentifierAwareJaxRsProvider;
+
+/**
+ * Common superclass for readers producing {@link PatchContext}.
+ *
+ * @author Robert Varga
+ */
+abstract class AbstractToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider<PatchContext> {
+
+ @Override
+ protected final PatchContext emptyBody(final InstanceIdentifierContext<?> path) {
+ return new PatchContext(path, null, null);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
+
+import com.google.common.base.Preconditions;
+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.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.ext.Provider;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
+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.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.codecs.StringModuleInstanceIdentifierCodec;
+import org.opendaylight.restconf.nb.rfc8040.handlers.DOMMountPointServiceHandler;
+import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
+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.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.SchemaNode;
+import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Provider
+@Consumes({Rfc8040.MediaTypes.PATCH + RestconfConstants.JSON})
+public class JsonToPatchBodyReader extends AbstractToPatchBodyReader {
+ private static final Logger LOG = LoggerFactory.getLogger(JsonToPatchBodyReader.class);
+
+ private String patchId;
+
+ @SuppressWarnings("checkstyle:IllegalCatch")
+ @Override
+ protected PatchContext readBody(final InstanceIdentifierContext<?> path, final InputStream entityStream)
+ throws IOException, WebApplicationException {
+ try {
+ return readFrom(path, entityStream);
+ } catch (final Exception e) {
+ throw propagateExceptionAs(e);
+ }
+ }
+
+ private PatchContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream)
+ throws IOException {
+ final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream));
+ final List<PatchEntity> resultList = read(jsonReader, path);
+ jsonReader.close();
+
+ return new PatchContext(path, resultList, patchId);
+ }
+
+ @SuppressWarnings("checkstyle:IllegalCatch")
+ public PatchContext readFrom(final String uriPath, final InputStream entityStream) throws
+ RestconfDocumentedException {
+ try {
+ return readFrom(
+ ParserIdentifier.toInstanceIdentifier(uriPath, SchemaContextHandler.getActualSchemaContext(),
+ DOMMountPointServiceHandler.getActualMountPointService()), entityStream);
+ } catch (final Exception e) {
+ propagateExceptionAs(e);
+ return null; // no-op
+ }
+ }
+
+ private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
+ if (exception instanceof RestconfDocumentedException) {
+ throw (RestconfDocumentedException)exception;
+ }
+
+ if (exception instanceof ResultAlreadySetException) {
+ LOG.debug("Error parsing json input:", exception);
+ throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
+ }
+
+ 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) 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);
+ 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 Draft11StringModuleInstanceIdentifierCodec codec
+ * @param resultCollection collection of parsed edits
+ * @throws IOException if operation fails
+ */
+ private void parseByName(@Nonnull final String name, @Nonnull final PatchEdit edit,
+ @Nonnull final JsonReader in, @Nonnull final InstanceIdentifierContext<?> path,
+ @Nonnull final StringModuleInstanceIdentifierCodec codec,
+ @Nonnull final List<PatchEntity> resultCollection) 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":
+ this.patchId = 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 Draft11StringModuleInstanceIdentifierCodec codec
+ * @throws IOException if operation fails
+ */
+ private void readEditDefinition(@Nonnull final PatchEdit edit, @Nonnull final JsonReader in,
+ @Nonnull final InstanceIdentifierContext<?> path,
+ @Nonnull final StringModuleInstanceIdentifierCodec codec) throws IOException {
+ String deferredValue = null;
+ 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()));
+ 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(path.getSchemaContext());
+ } else {
+ edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()).concat(target)));
+ edit.setTargetSchemaNode(SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
+ codec.getDataContextTree().getChild(edit.getTarget()).getDataSchemaNode().getPath()
+ .getParent()));
+ }
+
+ break;
+ case "value":
+ Preconditions.checkArgument(edit.getData() == null && deferredValue == null,
+ "Multiple value entries found");
+
+ if (edit.getTargetSchemaNode() == null) {
+ final StringBuilder sb = new StringBuilder();
+
+ // 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(sb, in);
+ deferredValue = sb.toString();
+ } else {
+ // We have a target schema node, reuse this reader without buffering the value.
+ edit.setData(readEditData(in, edit.getTargetSchemaNode(), path));
+ }
+ break;
+ default:
+ // FIXME: this does not look right, as it can wreck our logic
+ break;
+ }
+ }
+
+ in.endObject();
+
+ if (deferredValue != null) {
+ // read saved data to normalized node when target schema is already known
+ edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetSchemaNode(),
+ path));
+ }
+ }
+
+ /**
+ * Parse data defined in value node and saves it to buffer.
+ * @param sb Buffer to read value node
+ * @param in JsonReader reader
+ * @throws IOException if operation fails
+ */
+ private void readValueNode(@Nonnull final StringBuilder sb, @Nonnull final JsonReader in) throws IOException {
+ in.beginObject();
+
+ sb.append("{\"").append(in.nextName()).append("\":");
+
+ switch (in.peek()) {
+ case BEGIN_ARRAY:
+ in.beginArray();
+ sb.append('[');
+
+ while (in.hasNext()) {
+ if (in.peek() == JsonToken.STRING) {
+ sb.append('"').append(in.nextString()).append('"');
+ } else {
+ readValueObject(sb, in);
+ }
+ if (in.peek() != JsonToken.END_ARRAY) {
+ sb.append(',');
+ }
+ }
+
+ in.endArray();
+ sb.append(']');
+ break;
+ default:
+ readValueObject(sb, in);
+ break;
+ }
+
+ in.endObject();
+ sb.append('}');
+ }
+
+ /**
+ * Parse one value object of data and saves it to buffer.
+ * @param sb Buffer to read value object
+ * @param in JsonReader reader
+ * @throws IOException if operation fails
+ */
+ private void readValueObject(@Nonnull final StringBuilder sb, @Nonnull final JsonReader in) throws IOException {
+ // read simple leaf value
+ if (in.peek() == JsonToken.STRING) {
+ sb.append('"').append(in.nextString()).append('"');
+ return;
+ }
+
+ in.beginObject();
+ sb.append('{');
+
+ while (in.hasNext()) {
+ sb.append('"').append(in.nextName()).append("\":");
+
+ switch (in.peek()) {
+ case STRING:
+ sb.append('"').append(in.nextString()).append('"');
+ break;
+ case BEGIN_ARRAY:
+ in.beginArray();
+ sb.append('[');
+
+ while (in.hasNext()) {
+ if (in.peek() == JsonToken.STRING) {
+ sb.append('"').append(in.nextString()).append('"');
+ } else {
+ readValueObject(sb, in);
+ }
+
+ if (in.peek() != JsonToken.END_ARRAY) {
+ sb.append(',');
+ }
+ }
+
+ in.endArray();
+ sb.append(']');
+ break;
+ default:
+ readValueObject(sb, in);
+ }
+
+ if (in.peek() != JsonToken.END_OBJECT) {
+ sb.append(',');
+ }
+ }
+
+ in.endObject();
+ sb.append('}');
+ }
+
+ /**
+ * Read patch edit data defined in value node to NormalizedNode.
+ * @param in reader JsonReader reader
+ * @return NormalizedNode representing data
+ */
+ private static NormalizedNode<?, ?> readEditData(@Nonnull final JsonReader in,
+ @Nonnull final SchemaNode targetSchemaNode, @Nonnull final InstanceIdentifierContext<?> path) {
+ final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+ final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
+ JsonParserStream.create(writer, path.getSchemaContext(), 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(@Nonnull final PatchEdit edit) {
+ if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
+ && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
+ if (!edit.getOperation().isWithValue()) {
+ return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
+ }
+
+ // 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());
+ }
+
+ 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(@Nonnull final 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 SchemaNode targetSchemaNode;
+ private NormalizedNode<?, ?> data;
+
+ String getId() {
+ return id;
+ }
+
+ void setId(final String id) {
+ this.id = Preconditions.checkNotNull(id);
+ }
+
+ PatchEditOperation getOperation() {
+ return operation;
+ }
+
+ void setOperation(final PatchEditOperation operation) {
+ this.operation = Preconditions.checkNotNull(operation);
+ }
+
+ YangInstanceIdentifier getTarget() {
+ return target;
+ }
+
+ void setTarget(final YangInstanceIdentifier target) {
+ this.target = Preconditions.checkNotNull(target);
+ }
+
+ SchemaNode getTargetSchemaNode() {
+ return targetSchemaNode;
+ }
+
+ void setTargetSchemaNode(final SchemaNode targetSchemaNode) {
+ this.targetSchemaNode = Preconditions.checkNotNull(targetSchemaNode);
+ }
+
+ NormalizedNode<?, ?> getData() {
+ return data;
+ }
+
+ void setData(final NormalizedNode<?, ?> data) {
+ this.data = Preconditions.checkNotNull(data);
+ }
+
+ void clear() {
+ id = null;
+ operation = null;
+ target = null;
+ targetSchemaNode = null;
+ data = null;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.restconf.nb.rfc8040.jersey.providers.patch;
+
+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.restconf.common.errors.RestconfError;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
+import org.opendaylight.restconf.common.patch.PatchStatusEntity;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
+
+
+@Provider
+@Produces({ Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.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().getErrorTypeTag());
+ jsonWriter.name("error-tag").value(restconfError.getErrorTag().getTagValue());
+
+ // 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));
+ }
+}
--- /dev/null
+/*
+ * 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.restconf.nb.rfc8040.jersey.providers.patch;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.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.restconf.common.errors.RestconfError;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
+import org.opendaylight.restconf.common.patch.PatchStatusEntity;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+
+@Provider
+@Produces({ Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.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 IOException, WebApplicationException {
+
+ try {
+ final XMLStreamWriter xmlWriter =
+ XML_FACTORY.createXMLStreamWriter(entityStream, StandardCharsets.UTF_8.name());
+ writeDocument(xmlWriter, patchStatusContext);
+ } catch (final XMLStreamException e) {
+ throw new IllegalStateException(e);
+ } catch (final FactoryConfigurationError e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static void writeDocument(final XMLStreamWriter writer, final PatchStatusContext context)
+ throws XMLStreamException, IOException {
+ 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 IOException, XMLStreamException {
+ writer.writeStartElement("errors");
+
+ for (final RestconfError restconfError : errors) {
+ writer.writeStartElement("error-type");
+ writer.writeCharacters(restconfError.getErrorType().getErrorTypeTag());
+ writer.writeEndElement();
+
+ writer.writeStartElement("error-tag");
+ writer.writeCharacters(restconfError.getErrorTag().getTagValue());
+ 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();
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+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.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
+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.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.codecs.StringModuleInstanceIdentifierCodec;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.common.QName;
+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.util.SchemaContextUtil;
+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;
+
+@Provider
+@Consumes({Rfc8040.MediaTypes.PATCH + RestconfConstants.XML})
+public class XmlToPatchBodyReader extends AbstractToPatchBodyReader {
+ private static final Logger LOG = LoggerFactory.getLogger(XmlToPatchBodyReader.class);
+ private static final Splitter SLASH_SPLITTER = Splitter.on('/');
+
+ @SuppressWarnings("checkstyle:IllegalCatch")
+ @Override
+ protected PatchContext readBody(final InstanceIdentifierContext<?> path, final InputStream entityStream)
+ throws IOException, WebApplicationException {
+ try {
+ final Document doc = UntrustedXML.newDocumentBuilder().parse(entityStream);
+ return parse(path, doc);
+ } catch (final RestconfDocumentedException e) {
+ throw e;
+ } catch (final Exception e) {
+ LOG.debug("Error parsing xml input", e);
+
+ throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
+ ErrorTag.MALFORMED_MESSAGE);
+ }
+ }
+
+ 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());
+ 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().findModuleByNamespace(
+ URI.create(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;
+ if (target.equals("/")) {
+ targetII = pathContext.getInstanceIdentifier();
+ targetNode = pathContext.getSchemaContext();
+ } else {
+ targetII = codec.deserialize(codec.serialize(pathContext.getInstanceIdentifier())
+ .concat(prepareNonCondXpath(schemaNode, target.replaceFirst("/", ""), firstValueElement,
+ namespace, module.getQNameModule().getFormattedRevision())));
+
+ targetNode = SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+ codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath().getParent());
+
+ // move schema node
+ schemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+ codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath());
+ }
+
+ 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, pathContext.getSchemaContext(),
+ schemaNode);
+ 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(@Nonnull final Element element,
+ @Nonnull final 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(@Nonnull final DataSchemaNode schemaNode, @Nonnull final String target,
+ @Nonnull final Element value, @Nonnull final String namespace, @Nonnull final String revision) {
+ final Iterator<String> args = SLASH_SPLITTER.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("/");
+ nonCondXpath.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(@Nonnull final Element value, @Nonnull final 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(@Nonnull final StringBuilder nonCondXpath, @Nonnull final Iterator<QName> keyNames,
+ @Nonnull final Iterator<String> keyValues) {
+ while (keyNames.hasNext()) {
+ nonCondXpath.append('[');
+ nonCondXpath.append(keyNames.next().getLocalName());
+ nonCondXpath.append("='");
+ nonCondXpath.append(keyValues.next());
+ nonCondXpath.append("']");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.schema;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+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.restconf.common.schema.SchemaExportContext;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+
+@Provider
+@Produces({ Rfc8040.MediaTypes.YANG })
+public class SchemaExportContentYangBodyWriter implements MessageBodyWriter<SchemaExportContext> {
+
+ @Override
+ public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
+ final MediaType mediaType) {
+ return type.equals(SchemaExportContext.class);
+ }
+
+ @Override
+ public long getSize(final SchemaExportContext context, final Class<?> type, final Type genericType,
+ final Annotation[] annotations, final MediaType mediaType) {
+ return -1;
+ }
+
+ @Override
+ public void writeTo(final SchemaExportContext context, final Class<?> type, final Type genericType,
+ final Annotation[] annotations, final MediaType mediaType,
+ final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException,
+ WebApplicationException {
+ final PrintWriter writer = new PrintWriter(entityStream);
+ writer.write(context.getModule().getSource());
+
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.schema;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import javax.ws.rs.Produces;
+import javax.ws.rs.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.opendaylight.restconf.common.schema.SchemaExportContext;
+import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.yangtools.yang.model.export.YinExportUtils;
+
+@Provider
+@Produces({ Rfc8040.MediaTypes.YIN + RestconfConstants.XML })
+public class SchemaExportContentYinBodyWriter implements MessageBodyWriter<SchemaExportContext> {
+
+ @Override
+ public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
+ final MediaType mediaType) {
+ return type.equals(SchemaExportContext.class);
+ }
+
+ @Override
+ public long getSize(final SchemaExportContext context, final Class<?> type, final Type genericType,
+ final Annotation[] annotations, final MediaType mediaType) {
+ return -1;
+ }
+
+ @Override
+ public void writeTo(final SchemaExportContext context, final Class<?> type, final Type genericType,
+ final Annotation[] annotations, final MediaType mediaType,
+ final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException,
+ WebApplicationException {
+ try {
+ YinExportUtils.writeModuleToOutputStream(context.getSchemaContext(), context.getModule(), entityStream);
+ } catch (final XMLStreamException e) {
+ throw new IllegalStateException(e);
+ }
+
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.spi;
+
+import com.google.common.base.Optional;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.MessageBodyReader;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.nb.rfc8040.RestConnectorProvider;
+import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
+import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
+
+public abstract class AbstractIdentifierAwareJaxRsProvider<T> implements MessageBodyReader<T> {
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ private Request request;
+
+ @Override
+ public final boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations,
+ final MediaType mediaType) {
+ return true;
+ }
+
+ @Override
+ public final T readFrom(final Class<T> type, final Type genericType,
+ final Annotation[] annotations, final MediaType mediaType,
+ final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws IOException,
+ WebApplicationException {
+ final InstanceIdentifierContext<?> path = getInstanceIdentifierContext();
+ if (entityStream.available() < 1) {
+ return emptyBody(path);
+ }
+
+ return readBody(path, entityStream);
+ }
+
+ /**
+ * Create a type corresponding to an empty body.
+ *
+ * @param path Request path
+ * @return empty body type
+ */
+ protected abstract T emptyBody(InstanceIdentifierContext<?> path);
+
+ protected abstract T readBody(InstanceIdentifierContext<?> path, InputStream entityStream)
+ throws IOException, WebApplicationException;
+
+
+ private String getIdentifier() {
+ return this.uriInfo.getPathParameters(false).getFirst(RestconfConstants.IDENTIFIER);
+ }
+
+ private InstanceIdentifierContext<?> getInstanceIdentifierContext() {
+ return ParserIdentifier.toInstanceIdentifier(
+ getIdentifier(),
+ SchemaContextHandler.getActualSchemaContext(),
+ Optional.of(RestConnectorProvider.getMountPointService()));
+ }
+
+ protected UriInfo getUriInfo() {
+ return this.uriInfo;
+ }
+
+ protected boolean isPost() {
+ return HttpMethod.POST.equals(this.request.getMethod());
+ }
+
+ void setUriInfo(final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ void setRequest(final Request request) {
+ this.request = request;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2017 Pantheon Technologies, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.jersey.providers.spi;
+
+import com.google.common.annotations.Beta;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.UriInfo;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+
+/**
+ * Common superclass for readers producing {@link NormalizedNodeContext}.
+ *
+ * @author Robert Varga
+ */
+@Beta
+public abstract class AbstractNormalizedNodeBodyReader
+ extends AbstractIdentifierAwareJaxRsProvider<NormalizedNodeContext> {
+
+ public final void injectParams(final UriInfo uriInfo, final Request request) {
+ setUriInfo(uriInfo);
+ setRequest(request);
+ }
+
+ @Override
+ protected final NormalizedNodeContext emptyBody(final InstanceIdentifierContext<?> path) {
+ return new NormalizedNodeContext(path, null);
+ }
+}