package org.opendaylight.netconf.sal.rest.impl;
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.base.Predicate;
* the opposite of a {@link javax.xml.stream.XMLStreamReader} -- unlike instantiating an iterator over
* the backing data, this encapsulates a {@link NormalizedNodeStreamWriter} and allows
* us to write multiple nodes.
+ *
+ * @deprecated This class will be replaced by
+ * {@link org.opendaylight.restconf.jersey.providers.ParameterAwareNormalizedNodeWriter}
*/
-@Beta
+@Deprecated
public class DepthAwareNormalizedNodeWriter implements RestconfNormalizedNodeWriter {
private final NormalizedNodeStreamWriter writer;
protected int currentDepth = 0;
import org.opendaylight.netconf.sal.rest.api.RestconfService;
import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
-import org.opendaylight.restconf.Draft17;
-import org.opendaylight.restconf.utils.RestconfConstants;
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.model.api.SchemaNode;
import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+/**
+ * @deprecated This class will be replaced by
+ * {@link org.opendaylight.restconf.jersey.providers.NormalizedNodeJsonBodyWriter}
+ */
+@Deprecated
@Provider
@Produces({ Draft02.MediaTypes.API + RestconfService.JSON, Draft02.MediaTypes.DATA + RestconfService.JSON,
- Draft02.MediaTypes.OPERATION + RestconfService.JSON, Draft17.MediaTypes.DATA + RestconfConstants.JSON,
- MediaType.APPLICATION_JSON })
+ Draft02.MediaTypes.OPERATION + RestconfService.JSON, MediaType.APPLICATION_JSON })
public class NormalizedNodeJsonBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
private static final int DEFAULT_INDENT_SPACES_NUM = 2;
final SchemaPath path = context.getSchemaNode().getPath();
final JsonWriter jsonWriter = createJsonWriter(entityStream, t.getWriterParameters().isPrettyPrint());
jsonWriter.beginObject();
- writeNormalizedNode(jsonWriter,path,context,data, t.getWriterParameters().getDepth());
+ writeNormalizedNode(jsonWriter,path,context,data, Optional.fromNullable(t.getWriterParameters().getDepth()));
jsonWriter.endObject();
jsonWriter.flush();
}
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
+import javanet.staxutils.IndentingXMLStreamWriter;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import org.opendaylight.netconf.sal.rest.api.RestconfService;
import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
-import org.opendaylight.restconf.Draft17;
-import org.opendaylight.restconf.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.model.api.RpcDefinition;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.opendaylight.yangtools.yang.model.api.SchemaPath;
-import javanet.staxutils.IndentingXMLStreamWriter;
+/**
+ * @deprecated This class will be replaced by
+ * {@link org.opendaylight.restconf.jersey.providers.NormalizedNodeXmlBodyWriter}
+ */
+@Deprecated
@Provider
@Produces({ Draft02.MediaTypes.API + RestconfService.XML, Draft02.MediaTypes.DATA + RestconfService.XML,
- Draft02.MediaTypes.OPERATION + RestconfService.XML, Draft17.MediaTypes.DATA + RestconfConstants.XML,
- MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+ Draft02.MediaTypes.OPERATION + RestconfService.XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public class NormalizedNodeXmlBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
private static final XMLOutputFactory XML_FACTORY;
final NormalizedNode<?, ?> data = t.getData();
final SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
-
-
- writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, t.getWriterParameters().getDepth());
-
+ writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, Optional.fromNullable(t.getWriterParameters().getDepth()));
}
private void writeNormalizedNode(final XMLStreamWriter xmlWriter, final SchemaPath schemaPath, final InstanceIdentifierContext<?>
package org.opendaylight.netconf.sal.restconf.impl;
-import com.google.common.base.Optional;
+import java.util.List;
+import java.util.Set;
+import org.opendaylight.yangtools.yang.common.QName;
public class WriterParameters {
private final String content;
- private final Optional<Integer> depth;
+ private final Integer depth;
+ private final List<Set<QName>> fields;
private final boolean prettyPrint;
private WriterParameters(final WriterParametersBuilder builder) {
this.content = builder.content;
this.depth = builder.depth;
+ this.fields = builder.fields;
this.prettyPrint = builder.prettyPrint;
}
return content;
}
- public Optional<Integer> getDepth() {
+ public Integer getDepth() {
return depth;
}
+ public List<Set<QName>> getFields() {
+ return fields;
+ }
+
public boolean isPrettyPrint() {
return prettyPrint;
}
public static class WriterParametersBuilder {
private String content;
- private Optional<Integer> depth = Optional.absent();
+ private Integer depth;
+ private List<Set<QName>> fields;
private boolean prettyPrint;
public WriterParametersBuilder() {}
}
public WriterParametersBuilder setDepth(final int depth) {
- this.depth = Optional.of(depth);
+ this.depth = depth;
+ return this;
+ }
+
+ public WriterParametersBuilder setFields(final List<Set<QName>> fields) {
+ this.fields = fields;
return this;
}
import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContentYangBodyWriter;
import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContentYinBodyWriter;
import org.opendaylight.netconf.sal.rest.impl.JsonNormalizedNodeBodyReader;
-import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter;
-import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeXmlBodyWriter;
import org.opendaylight.netconf.sal.rest.impl.PATCHJsonBodyWriter;
import org.opendaylight.netconf.sal.rest.impl.PATCHXmlBodyWriter;
import org.opendaylight.netconf.sal.rest.impl.RestconfDocumentedExceptionMapper;
import org.opendaylight.netconf.sal.rest.impl.XmlNormalizedNodeBodyReader;
import org.opendaylight.restconf.common.wrapper.services.ServicesWrapperImpl;
import org.opendaylight.restconf.jersey.providers.JsonToPATCHBodyReader;
+import org.opendaylight.restconf.jersey.providers.NormalizedNodeJsonBodyWriter;
+import org.opendaylight.restconf.jersey.providers.NormalizedNodeXmlBodyWriter;
import org.opendaylight.restconf.jersey.providers.XmlToPATCHBodyReader;
public class RestconfApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
- return ImmutableSet.<Class<?>> builder().add(NormalizedNodeJsonBodyWriter.class)
- .add(NormalizedNodeXmlBodyWriter.class).add(JsonNormalizedNodeBodyReader.class)
- .add(XmlNormalizedNodeBodyReader.class).add(SchemaExportContentYinBodyWriter.class)
+ return ImmutableSet.<Class<?>> builder()
+ .add(NormalizedNodeJsonBodyWriter.class).add(NormalizedNodeXmlBodyWriter.class)
+ .add(JsonNormalizedNodeBodyReader.class).add(XmlNormalizedNodeBodyReader.class)
+ .add(SchemaExportContentYinBodyWriter.class)
.add(JsonToPATCHBodyReader.class).add(XmlToPATCHBodyReader.class)
.add(PATCHJsonBodyWriter.class).add(PATCHXmlBodyWriter.class)
.add(SchemaExportContentYangBodyWriter.class).add(RestconfDocumentedExceptionMapper.class)
import org.opendaylight.restconf.rest.services.impl.RestconfOperationsServiceImpl;
import org.opendaylight.restconf.rest.services.impl.RestconfSchemaServiceImpl;
import org.opendaylight.restconf.rest.services.impl.RestconfStreamsServiceImpl;
-import org.opendaylight.restconf.restful.services.api.TransactionServicesWrapper;
import org.opendaylight.restconf.restful.services.api.RestconfDataService;
import org.opendaylight.restconf.restful.services.api.RestconfInvokeOperationsService;
import org.opendaylight.restconf.restful.services.api.RestconfStreamsSubscriptionService;
+import org.opendaylight.restconf.restful.services.api.TransactionServicesWrapper;
import org.opendaylight.restconf.restful.services.impl.RestconfDataServiceImpl;
import org.opendaylight.restconf.restful.services.impl.RestconfInvokeOperationsServiceImpl;
import org.opendaylight.restconf.restful.services.impl.RestconfStreamsSubscriptionServiceImpl;
return this.delegRestSchService.getSchema(mountAndModuleId);
}
+ @Override
+ public Response readData(final UriInfo uriInfo) {
+ return this.delegRestconfDataService.readData(uriInfo);
+ }
+
@Override
public Response readData(final String identifier, final UriInfo uriInfo) {
return this.delegRestconfDataService.readData(identifier, uriInfo);
--- /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.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.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
+import org.opendaylight.restconf.Draft17;
+import org.opendaylight.restconf.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({ Draft17.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 t,
+ final Class<?> type,
+ final Type genericType,
+ final Annotation[] annotations,
+ final MediaType mediaType) {
+ return -1;
+ }
+
+ @Override
+ public void writeTo(final NormalizedNodeContext t,
+ 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 = t.getData();
+ if (data == null) {
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ final InstanceIdentifierContext<SchemaNode> context =
+ (InstanceIdentifierContext<SchemaNode>) t.getInstanceIdentifierContext();
+ final SchemaPath path = context.getSchemaNode().getPath();
+ final JsonWriter jsonWriter = createJsonWriter(entityStream,
+ t.getWriterParameters().isPrettyPrint());
+
+ jsonWriter.beginObject();
+ writeNormalizedNode(jsonWriter, path, context, data,
+ t.getWriterParameters().getDepth(), t.getWriterParameters().getFields());
+ jsonWriter.endObject();
+ jsonWriter.flush();
+ }
+
+ private 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 void writeChildren(final RestconfNormalizedNodeWriter nnWriter,
+ final ContainerNode data) throws IOException {
+ for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
+ nnWriter.write(child);
+ }
+ }
+
+ private 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 JsonWriter createJsonWriter(final OutputStream entityStream, final boolean prettyPrint) {
+ if (prettyPrint) {
+ return JsonWriterFactory.createJsonWriter(
+ new OutputStreamWriter(entityStream, StandardCharsets.UTF_8), DEFAULT_INDENT_SPACES_NUM);
+ } else {
+ return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, StandardCharsets.UTF_8));
+ }
+ }
+
+ private JSONCodecFactory getCodecFactory(final InstanceIdentifierContext<?> context) {
+ // TODO: Performance: Cache JSON Codec factory and schema context
+ return JSONCodecFactory.create(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.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.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.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
+import org.opendaylight.restconf.Draft17;
+import org.opendaylight.restconf.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.impl.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({ Draft17.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 t,
+ final Class<?> type,
+ final Type genericType,
+ final Annotation[] annotations,
+ final MediaType mediaType) {
+ return -1;
+ }
+
+ @Override
+ public void writeTo(final NormalizedNodeContext t,
+ 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 = t.getInstanceIdentifierContext();
+ if (t.getData() == null) {
+ return;
+ }
+
+ XMLStreamWriter xmlWriter;
+ try {
+ xmlWriter = XML_FACTORY.createXMLStreamWriter(entityStream);
+ if (t.getWriterParameters().isPrettyPrint()) {
+ xmlWriter = new IndentingXMLStreamWriter(xmlWriter);
+ }
+ } catch (final XMLStreamException | FactoryConfigurationError e) {
+ throw new IllegalStateException(e);
+ }
+ final NormalizedNode<?, ?> data = t.getData();
+ final SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
+
+ writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, t.getWriterParameters().getDepth(),
+ t.getWriterParameters().getFields());
+ }
+
+ private 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 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 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.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.base.Predicate;
+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.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.AnyXmlNode;
+import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.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) {
+ if (orderKeyLeaves) {
+ return new OrderedParameterAwareNormalizedNodeWriter(writer, maxDepth, fields);
+ } else {
+ return new ParameterAwareNormalizedNodeWriter(writer, maxDepth, fields);
+ }
+ }
+
+ /**
+ * Iterate over the provided {@link NormalizedNode} and emit write
+ * events to the encapsulated {@link NormalizedNodeStreamWriter}.
+ *
+ * @param node Node
+ * @return
+ * @throws IOException when thrown from the backing writer.
+ */
+ 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()) {
+ if (fields.get(currentDepth - 1).contains(node.getNodeType())) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // 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(), new Predicate<NormalizedNode<?, ?>>() {
+ @Override
+ public boolean apply(final NormalizedNode<?, ?> 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;
+ }
+ }
+}
@Path("/data/{identifier:.+}")
@Produces({ Draft17.MediaTypes.DATA + RestconfConstants.JSON, Draft17.MediaTypes.DATA, MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML, MediaType.TEXT_XML })
- Response readData(@Encoded @PathParam("identifier") String identifier,
- @Context UriInfo uriInfo);
+ Response readData(@Encoded @PathParam("identifier") String identifier, @Context UriInfo uriInfo);
+
+ /**
+ * Get target data resource from data root.
+ *
+ * @param uriInfo
+ * - URI info
+ * @return {@link NormalizedNodeContext}
+ */
+ @GET
+ @Path("/data")
+ @Produces({ Draft17.MediaTypes.DATA + RestconfConstants.JSON, Draft17.MediaTypes.DATA, MediaType.APPLICATION_JSON,
+ MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+ Response readData(@Context UriInfo uriInfo);
/**
* Create or replace the target data resource.
}
@Override
- public Response readData(final String identifier, final UriInfo uriInfo) {
- Preconditions.checkNotNull(identifier);
+ public Response readData(final UriInfo uriInfo) {
+ return readData(null, uriInfo);
+ }
- final WriterParameters parameters = ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ @Override
+ public Response readData(final String identifier, final UriInfo uriInfo) {
final SchemaContextRef schemaContextRef = new SchemaContextRef(this.schemaContextHandler.get());
-
final InstanceIdentifierContext<?> instanceIdentifier = ParserIdentifier.toInstanceIdentifier(
identifier, schemaContextRef.get(), Optional.of(this.mountPointServiceHandler.get()));
- final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
+ final WriterParameters parameters = ReadDataTransactionUtil.parseUriParameters(
+ instanceIdentifier, uriInfo);
+
+ final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
final DOMTransactionChain transactionChain;
if (mountPoint == null) {
transactionChain = this.transactionChainHandler.get();
import javax.ws.rs.core.UriInfo;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
import org.opendaylight.netconf.sal.restconf.impl.RestconfError;
import org.opendaylight.netconf.sal.restconf.impl.WriterParameters;
import org.opendaylight.netconf.sal.restconf.impl.WriterParameters.WriterParametersBuilder;
import org.opendaylight.restconf.restful.transaction.TransactionVarsWrapper;
+import org.opendaylight.restconf.utils.parser.ParserFieldsParameter;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
/**
* Parse parameters from URI request and check their types and values.
*
+ *
+ * @param identifier
+ * - {@link InstanceIdentifierContext}
* @param uriInfo
* - URI info
* @return {@link WriterParameters}
*/
- @Nonnull public static WriterParameters parseUriParameters(@Nullable final UriInfo uriInfo) {
+ public static @Nonnull WriterParameters parseUriParameters(@Nonnull final InstanceIdentifierContext<?> identifier,
+ @Nullable final UriInfo uriInfo) {
final WriterParametersBuilder builder = new WriterParametersBuilder();
if (uriInfo == null) {
RestconfDataServiceConstant.ReadData.READ_TYPE_TX,
uriInfo.getQueryParameters().keySet(),
RestconfDataServiceConstant.ReadData.CONTENT,
- RestconfDataServiceConstant.ReadData.DEPTH);
+ RestconfDataServiceConstant.ReadData.DEPTH,
+ RestconfDataServiceConstant.ReadData.FIELDS);
// read parameters from URI or set default values
final List<String> content = uriInfo.getQueryParameters().getOrDefault(
final List<String> depth = uriInfo.getQueryParameters().getOrDefault(
RestconfDataServiceConstant.ReadData.DEPTH,
Collections.singletonList(RestconfDataServiceConstant.ReadData.UNBOUNDED));
+ // fields
+ final List<String> fields = uriInfo.getQueryParameters().getOrDefault(
+ RestconfDataServiceConstant.ReadData.FIELDS,
+ Collections.emptyList());
// parameter can be in URI at most once
ParametersUtil.checkParameterCount(content, RestconfDataServiceConstant.ReadData.CONTENT);
ParametersUtil.checkParameterCount(depth, RestconfDataServiceConstant.ReadData.DEPTH);
+ ParametersUtil.checkParameterCount(fields, RestconfDataServiceConstant.ReadData.FIELDS);
// check and set content
final String contentValue = content.get(0);
}
}
+ // check and set fields
+ if (!fields.isEmpty()) {
+ builder.setFields(ParserFieldsParameter.parseFieldsParameter(identifier, fields.get(0)));
+ }
+
return builder.build();
}
// URI parameters
public static final String CONTENT = "content";
public static final String DEPTH = "depth";
+ public static final String FIELDS = "fields";
// content values
public static final String CONFIG = "config";
--- /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.utils.parser;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.restconf.utils.parser.builder.ParserBuilderConstants.Deserializer;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+public class ParserFieldsParameter {
+ private static final char COLON = ':';
+ private static final char SEMICOLON = ';';
+ private static final char SLASH = '/';
+ private static final char STARTING_PARENTHESIS = '(';
+ private static final char CLOSING_PARENTHESIS = ')';
+
+ /**
+ * Parse fields parameter and return complete list of child nodes organized into levels.
+ * @param identifier identifier context created from request URI
+ * @param input input value of fields parameter
+ * @return {@link List}
+ */
+ public static @Nonnull List<Set<QName>> parseFieldsParameter(@Nonnull final InstanceIdentifierContext<?> identifier,
+ @Nonnull final String input) {
+ final List<Set<QName>> parsed = new ArrayList<>();
+ final SchemaContext context = identifier.getSchemaContext();
+ final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
+ final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
+ (DataSchemaNode) identifier.getSchemaNode());
+
+ if (startNode == null) {
+ throw new RestconfDocumentedException(
+ "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ parseInput(input, startQNameModule, startNode, parsed, context);
+ return parsed;
+ }
+
+ /**
+ * Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
+ * @param input input value of fields parameter
+ * @param startQNameModule starting qname module
+ * @param startNode starting node
+ * @param parsed list of results
+ * @param context schema context
+ */
+ private static void parseInput(@Nonnull final String input,
+ @Nonnull final QNameModule startQNameModule,
+ @Nonnull final DataSchemaContextNode<?> startNode,
+ @Nonnull final List<Set<QName>> parsed,
+ @Nonnull final SchemaContext context) {
+ int currentPosition = 0;
+ int startPosition = 0;
+ DataSchemaContextNode<?> currentNode = startNode;
+ QNameModule currentQNameModule = startQNameModule;
+
+ Set<QName> currentLevel = new HashSet<>();
+ parsed.add(currentLevel);
+
+ while (currentPosition < input.length()) {
+ final char currentChar = input.charAt(currentPosition);
+
+ if (Deserializer.IDENTIFIER.matches(currentChar) || currentChar == '/') {
+ if (currentChar == SLASH) {
+ // add parsed identifier to results for current level
+ currentNode = addChildToResult(
+ currentNode,
+ input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
+ // go one level down
+ currentLevel = new HashSet<>();
+ parsed.add(currentLevel);
+
+ currentPosition++;
+ startPosition = currentPosition;
+ } else {
+ currentPosition++;
+ }
+
+ continue;
+ }
+
+ switch (currentChar) {
+ case COLON :
+ // new namespace and revision found
+ currentQNameModule = context.findModuleByName(
+ input.substring(startPosition, currentPosition), null).getQNameModule();
+ currentPosition++;
+ break;
+ case STARTING_PARENTHESIS:
+ // add current child to parsed results for current level
+ final DataSchemaContextNode<?> child = addChildToResult(
+ currentNode,
+ input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
+ // call with child node as new start node for one level down
+ int closingParenthesis = currentPosition
+ + findClosingParenthesis(input.substring(currentPosition + 1));
+ parseInput(
+ input.substring(currentPosition + 1, closingParenthesis),
+ currentQNameModule,
+ child,
+ parsed,
+ context);
+
+ // closing parenthesis must be at the end of input or separator and one more character is expected
+ currentPosition = closingParenthesis + 1;
+ if (currentPosition != input.length()) {
+ if (currentPosition + 1 < input.length()) {
+ if (input.charAt(currentPosition) == SEMICOLON) {
+ currentPosition++;
+ } else {
+ throw new RestconfDocumentedException(
+ "Missing semicolon character after "
+ + child.getIdentifier().getNodeType().getLocalName()
+ + " child nodes",
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+ } else {
+ throw new RestconfDocumentedException(
+ "Unexpected character '"
+ + input.charAt(currentPosition)
+ + "' found in fields parameter value",
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+ }
+
+ break;
+ case SEMICOLON:
+ // complete identifier found
+ addChildToResult(
+ currentNode,
+ input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
+ currentPosition++;
+ break;
+ default:
+ throw new RestconfDocumentedException(
+ "Unexpected character '" + currentChar + "' found in fields parameter value",
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ startPosition = currentPosition;
+ }
+
+ // parse input to end
+ if (startPosition < input.length()) {
+ addChildToResult(currentNode, input.substring(startPosition), currentQNameModule, currentLevel);
+ }
+ }
+
+ /**
+ * Add parsed child of current node to result for current level.
+ * @param currentNode current node
+ * @param identifier parsed identifier of child node
+ * @param currentQNameModule current namespace and revision in {@link QNameModule}
+ * @param level current nodes level
+ * @return {@link DataSchemaContextNode}
+ */
+ private static @Nonnull DataSchemaContextNode<?> addChildToResult(
+ @Nonnull final DataSchemaContextNode<?> currentNode,
+ @Nonnull final String identifier,
+ @Nonnull final QNameModule currentQNameModule,
+ @Nonnull final Set<QName> level) {
+ final QName childQName = QName.create(
+ currentQNameModule.getNamespace().toString(),
+ identifier,
+ currentQNameModule.getRevision());
+
+ // resolve parent node
+ final DataSchemaContextNode<?> parentNode = resolveMixinNode(
+ currentNode, level, currentNode.getIdentifier().getNodeType());
+ if (parentNode == null) {
+ throw new RestconfDocumentedException(
+ "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ // resolve child node
+ final DataSchemaContextNode<?> childNode = resolveMixinNode(
+ parentNode.getChild(childQName), level, childQName);
+ if (childNode == null) {
+ throw new RestconfDocumentedException(
+ "Child " + identifier + " node missing in "
+ + currentNode.getIdentifier().getNodeType().getLocalName(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ // add final childNode node to level nodes
+ level.add(childNode.getIdentifier().getNodeType());
+ return childNode;
+ }
+
+ /**
+ * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
+ * All nodes expect of not mixin node are added to current level nodes.
+ * @param node initial mixin or not-mixin node
+ * @param level current nodes level
+ * @param qName qname of initial node
+ * @return {@link DataSchemaContextNode}
+ */
+ private static @Nullable DataSchemaContextNode<?> resolveMixinNode(@Nullable final DataSchemaContextNode<?> node,
+ @Nonnull final Set<QName> level,
+ @Nonnull final QName qName) {
+ DataSchemaContextNode<?> currentNode = node;
+ while (currentNode != null && currentNode.isMixin()) {
+ level.add(qName);
+ currentNode = currentNode.getChild(qName);
+ }
+
+ return currentNode;
+ }
+
+ /**
+ * Find position of matching parenthesis increased by one, but at most equals to input size.
+ * @param input input where to find for closing parenthesis
+ * @return int position of closing parenthesis increased by one
+ */
+ private static int findClosingParenthesis(@Nonnull final String input) {
+ int position = 0;
+ int count = 1;
+
+ while (position < input.length()) {
+ final char currentChar = input.charAt(position);
+
+ if (currentChar == STARTING_PARENTHESIS) {
+ count++;
+ }
+
+ if (currentChar == CLOSING_PARENTHESIS) {
+ count--;
+ }
+
+ if (count == 0) {
+ break;
+ }
+
+ position++;
+ }
+
+ // closing parenthesis was not found
+ if (position >= input.length()) {
+ throw new RestconfDocumentedException("Missing closing parenthesis in fields parameter",
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ return ++position;
+ }
+}
--- /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.jersey.providers;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+
+/**
+ * Unit test for {@link ParameterAwareNormalizedNodeWriter} used with depth parameter.
+ */
+public class ParameterAwareNormalizedNodeWriterDepthTest {
+
+ @Mock
+ private NormalizedNodeStreamWriter writer;
+ @Mock
+ private ContainerNode containerNodeData;
+ @Mock
+ private MapNode mapNodeData;
+ @Mock
+ private MapEntryNode mapEntryNodeData;
+ @Mock
+ private LeafSetNode<String> leafSetNodeData;
+ @Mock
+ private LeafSetEntryNode<String> leafSetEntryNodeData;
+ @Mock
+ private LeafNode<String> keyLeafNodeData;
+ @Mock
+ private LeafNode<String> anotherLeafNodeData;
+
+ private NodeIdentifier containerNodeIdentifier;
+ private NodeIdentifier mapNodeIdentifier;
+ private NodeIdentifierWithPredicates mapEntryNodeIdentifier;
+ private NodeIdentifier leafSetNodeIdentifier;
+ private NodeWithValue<?> leafSetEntryNodeIdentifier;
+ private NodeIdentifier keyLeafNodeIdentifier;
+ private NodeIdentifier anotherLeafNodeIdentifier;
+
+ private Collection<DataContainerChild<?, ?>> containerNodeValue;
+ private Collection<MapEntryNode> mapNodeValue;
+ private Collection<DataContainerChild<?, ?>> mapEntryNodeValue;
+ private Collection<LeafSetEntryNode<String>> leafSetNodeValue;
+ private String leafSetEntryNodeValue;
+ private String keyLeafNodeValue;
+ private String anotherLeafNodeValue;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ // identifiers
+ containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container"));
+ Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier);
+ Mockito.when(containerNodeData.getNodeType()).thenReturn(containerNodeIdentifier.getNodeType());
+
+ mapNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "list"));
+ Mockito.when(mapNodeData.getIdentifier()).thenReturn(mapNodeIdentifier);
+
+ final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry");
+ leafSetEntryNodeValue = "leaf-set-value";
+ leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue);
+ Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier);
+ Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName);
+
+ leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set"));
+ Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier);
+
+ final QName mapEntryNodeKey = QName.create("namespace", "key-field");
+ keyLeafNodeIdentifier = NodeIdentifier.create(mapEntryNodeKey);
+ keyLeafNodeValue = "key-value";
+
+ mapEntryNodeIdentifier = new YangInstanceIdentifier.NodeIdentifierWithPredicates(
+ QName.create("namespace", "list-entry"), Collections.singletonMap(mapEntryNodeKey, keyLeafNodeValue));
+ Mockito.when(mapEntryNodeData.getIdentifier()).thenReturn(mapEntryNodeIdentifier);
+ Mockito.when(mapEntryNodeData.getChild(keyLeafNodeIdentifier)).thenReturn(Optional.of(keyLeafNodeData));
+
+ Mockito.when(keyLeafNodeData.getValue()).thenReturn(keyLeafNodeValue);
+ Mockito.when(keyLeafNodeData.getIdentifier()).thenReturn(keyLeafNodeIdentifier);
+
+ anotherLeafNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "another-field"));
+ anotherLeafNodeValue = "another-value";
+
+ Mockito.when(anotherLeafNodeData.getValue()).thenReturn(anotherLeafNodeValue);
+ Mockito.when(anotherLeafNodeData.getIdentifier()).thenReturn(anotherLeafNodeIdentifier);
+
+ // values
+ Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue);
+
+ leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData);
+ Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue);
+
+ containerNodeValue = Collections.singleton(leafSetNodeData);
+ Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue);
+
+ mapEntryNodeValue = Sets.newHashSet(keyLeafNodeData, anotherLeafNodeData);
+ Mockito.when(mapEntryNodeData.getValue()).thenReturn(mapEntryNodeValue);
+
+ mapNodeValue = Collections.singleton(mapEntryNodeData);
+ Mockito.when(mapNodeData.getValue()).thenReturn(mapNodeValue);
+ }
+
+ /**
+ * Test write {@link ContainerNode} with children but write data only to depth 1 (children will not be written).
+ * Depth parameter is limited to 1.
+ */
+ @Test
+ public void writeContainerWithoutChildrenDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter
+ .forStreamWriter(writer, 1, null);
+
+ parameterWriter.write(containerNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write {@link ContainerNode} with children and write also all its children.
+ * Depth parameter has higher value than maximal children depth.
+ */
+ @Test
+ public void writeContainerWithChildrenDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, Integer.MAX_VALUE, null);
+
+ parameterWriter.write(containerNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+ leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link MapNode} with children but write data only to depth 1 (children will not be written).
+ * Depth parameter limits depth to 1.
+ */
+ @Test
+ public void writeMapNodeWithoutChildrenDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter
+ .forStreamWriter(writer, 1, null);
+
+ parameterWriter.write(mapNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write {@link MapNode} with children and write also all its children.
+ * Depth parameter has higher value than maximal children depth.
+ *
+ *
+ * FIXME
+ * Although ordered writer is used leaves are not written in expected order.
+ *
+ */
+ @Ignore
+ @Test
+ public void writeMapNodeWithChildrenDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, Integer.MAX_VALUE, null);
+
+ parameterWriter.write(mapNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ inOrder.verify(writer, Mockito.times(2)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+ // FIXME this assertion is not working because leaves are not written in expected order
+ inOrder.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue);
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link LeafSetNode} with depth 1 (children will not be written).
+ * Depth parameter limits depth to 1.
+ */
+ @Test
+ public void writeLeafSetNodeWithoutChildrenDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, 1, null);
+
+ parameterWriter.write(leafSetNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link LeafSetNode} when all its children will be written.
+ * Depth parameter has higher value than maximal children depth.
+ */
+ @Test
+ public void writeLeafSetNodeWithChildrenDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, Integer.MAX_VALUE, null);
+
+ parameterWriter.write(leafSetNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+ leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link LeafSetEntryNode}.
+ * Depth parameter has higher value than maximal children depth.
+ */
+ @Test
+ public void writeLeafSetEntryNodeDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, Integer.MAX_VALUE, null);
+
+ parameterWriter.write(leafSetEntryNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+ leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link MapEntryNode} unordered to depth 1 to write only keys.
+ * Depth parameter limits depth to 1.
+ */
+ @Test
+ public void writeMapEntryNodeUnorderedOnlyKeysDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, false, 1, null);
+
+ parameterWriter.write(mapEntryNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ // write only the key
+ inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link MapEntryNode} unordered with full depth.
+ * Depth parameter has higher value than maximal children depth.
+ */
+ @Test
+ public void writeMapEntryNodeUnorderedDepthTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, false, Integer.MAX_VALUE, null);
+
+ parameterWriter.write(mapEntryNodeData);
+
+ // unordered
+ Mockito.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ Mockito.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+ Mockito.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue);
+ Mockito.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link MapEntryNode} ordered with depth 1 (children will not be written).
+ * Depth parameter limits depth to 1.
+ */
+ @Test
+ public void writeMapEntryNodeOrderedWithoutChildrenTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, true, 1, null);
+
+ parameterWriter.write(mapEntryNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write with {@link MapEntryNode} ordered and write also all its children.
+ * Depth parameter has higher value than maximal children depth.
+ *
+ * FIXME
+ * Although ordered writer is used leaves are not written in expected order.
+ *
+ */
+ @Ignore
+ @Test
+ public void writeMapEntryNodeOrderedTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, true, Integer.MAX_VALUE, null);
+
+ parameterWriter.write(mapEntryNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ inOrder.verify(writer, Mockito.times(2)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+ // FIXME this assertion is not working because leaves are not written in expected order
+ inOrder.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue);
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+}
\ No newline at end of file
--- /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.jersey.providers;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+
+/**
+ * Unit test for {@link ParameterAwareNormalizedNodeWriter} used with fields parameter.
+ */
+public class ParameterAwareNormalizedNodeWriterFieldsTest {
+
+ @Mock
+ private NormalizedNodeStreamWriter writer;
+ @Mock
+ private ContainerNode containerNodeData;
+ @Mock
+ private MapNode mapNodeData;
+ @Mock
+ private MapEntryNode mapEntryNodeData;
+ @Mock
+ private LeafSetNode<String> leafSetNodeData;
+ @Mock
+ private LeafSetEntryNode<String> leafSetEntryNodeData;
+ @Mock
+ private LeafNode<String> keyLeafNodeData;
+
+ private NodeIdentifier containerNodeIdentifier;
+ private NodeIdentifier mapNodeIdentifier;
+ private NodeIdentifierWithPredicates mapEntryNodeIdentifier;
+ private NodeIdentifier leafSetNodeIdentifier;
+ private NodeWithValue<?> leafSetEntryNodeIdentifier;
+ private NodeIdentifier keyLeafNodeIdentifier;
+
+ private Collection<DataContainerChild<?, ?>> containerNodeValue;
+ private Collection<MapEntryNode> mapNodeValue;
+ private Collection<DataContainerChild<?, ?>> mapEntryNodeValue;
+ private Collection<LeafSetEntryNode<String>> leafSetNodeValue;
+ private String leafSetEntryNodeValue;
+ private String keyLeafNodeValue;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ // identifiers
+ containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container"));
+ Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier);
+ Mockito.when(containerNodeData.getNodeType()).thenReturn(containerNodeIdentifier.getNodeType());
+
+ mapNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "list"));
+ Mockito.when(mapNodeData.getIdentifier()).thenReturn(mapNodeIdentifier);
+ Mockito.when(mapNodeData.getNodeType()).thenReturn(mapNodeIdentifier.getNodeType());
+
+ final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry");
+ leafSetEntryNodeValue = "leaf-set-value";
+ leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue);
+ Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier);
+ Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeIdentifier.getNodeType());
+ Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName);
+
+ leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set"));
+ Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier);
+ Mockito.when(leafSetNodeData.getNodeType()).thenReturn(leafSetNodeIdentifier.getNodeType());
+
+ final QName mapEntryNodeKey = QName.create("namespace", "key-field");
+ keyLeafNodeIdentifier = NodeIdentifier.create(mapEntryNodeKey);
+ keyLeafNodeValue = "key-value";
+
+ mapEntryNodeIdentifier = new NodeIdentifierWithPredicates(
+ QName.create("namespace", "list-entry"), Collections.singletonMap(mapEntryNodeKey, keyLeafNodeValue));
+ Mockito.when(mapEntryNodeData.getIdentifier()).thenReturn(mapEntryNodeIdentifier);
+ Mockito.when(mapEntryNodeData.getNodeType()).thenReturn(mapEntryNodeIdentifier.getNodeType());
+ Mockito.when(mapEntryNodeData.getChild(keyLeafNodeIdentifier)).thenReturn(Optional.of(keyLeafNodeData));
+
+ Mockito.when(keyLeafNodeData.getValue()).thenReturn(keyLeafNodeValue);
+ Mockito.when(keyLeafNodeData.getIdentifier()).thenReturn(keyLeafNodeIdentifier);
+ Mockito.when(keyLeafNodeData.getNodeType()).thenReturn(keyLeafNodeIdentifier.getNodeType());
+
+ // values
+ Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue);
+
+ leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData);
+ Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue);
+
+ containerNodeValue = Collections.singleton(leafSetNodeData);
+ Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue);
+
+ mapEntryNodeValue = Sets.newHashSet(keyLeafNodeData);
+ Mockito.when(mapEntryNodeData.getValue()).thenReturn(mapEntryNodeValue);
+
+ mapNodeValue = Collections.singleton(mapEntryNodeData);
+ Mockito.when(mapNodeData.getValue()).thenReturn(mapNodeValue);
+ }
+
+ /**
+ * Test write {@link ContainerNode} when children which will be written are limited.
+ * Fields parameter selects 0/1 of container children to be written.
+ */
+ @Test
+ public void writeContainerWithLimitedFieldsTest() throws Exception {
+ final List<Set<QName>> limitedFields = new ArrayList<>();
+ limitedFields.add(Sets.newHashSet());
+
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, null, limitedFields);
+
+ parameterWriter.write(containerNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write {@link ContainerNode} when all its children are selected to be written.
+ * Fields parameter selects 1/1 of container children to be written.
+ */
+ @Test
+ public void writeContainerAllFieldsTest() throws Exception {
+ final List<Set<QName>> limitedFields = new ArrayList<>();
+ limitedFields.add(Sets.newHashSet(leafSetNodeIdentifier.getNodeType()));
+
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, null, limitedFields);
+
+ parameterWriter.write(containerNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+ leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write {@link MapEntryNode} as child of {@link MapNode} when children which will be written are limited.
+ * Fields parameter selects 0/1 of map entry node children to be written.
+ */
+ @Test
+ public void writeMapEntryNodeWithLimitedFieldsTest() throws Exception {
+ final List<Set<QName>> limitedFields = new ArrayList<>();
+ limitedFields.add(Sets.newHashSet());
+
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, null, limitedFields);
+
+ parameterWriter.write(mapNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write {@link MapEntryNode} as child of {@link MapNode} when all its children will be written.
+ * Fields parameter selects 1/1 of map entry node children to be written.
+ */
+ @Test
+ public void writeMapNodeAllFieldsTest() throws Exception {
+ final List<Set<QName>> limitedFields = new ArrayList<>();
+ limitedFields.add(Sets.newHashSet(keyLeafNodeData.getNodeType()));
+
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, null, limitedFields);
+
+ parameterWriter.write(mapNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+}
\ No newline at end of file
--- /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.jersey.providers;
+
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+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.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+
+/**
+ * Unit test for {@link ParameterAwareNormalizedNodeWriter} used with all parameters.
+ */
+public class ParameterAwareNormalizedNodeWriterParametersTest {
+
+ @Mock
+ private NormalizedNodeStreamWriter writer;
+ @Mock
+ private ContainerNode containerNodeData;
+ @Mock
+ private LeafSetNode<String> leafSetNodeData;
+ @Mock
+ private LeafSetEntryNode<String> leafSetEntryNodeData;
+ @Mock
+ private ContainerNode rootDataContainerData;
+
+ private NodeIdentifier containerNodeIdentifier;
+ private NodeIdentifier leafSetNodeIdentifier;
+ private NodeWithValue<?> leafSetEntryNodeIdentifier;
+ private NodeIdentifier rootDataContainerIdentifier;
+
+ private Collection<DataContainerChild<?, ?>> containerNodeValue;
+ private Collection<LeafSetEntryNode<String>> leafSetNodeValue;
+ private String leafSetEntryNodeValue;
+ private Collection<DataContainerChild<?, ?>> rootDataContainerValue;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ // identifiers
+ containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container"));
+ Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier);
+ Mockito.when(containerNodeData.getNodeType()).thenReturn(containerNodeIdentifier.getNodeType());
+
+ final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry");
+ leafSetEntryNodeValue = "leaf-set-value";
+ leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue);
+ Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier);
+ Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeIdentifier.getNodeType());
+ Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName);
+
+ leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set"));
+ Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier);
+ Mockito.when(leafSetNodeData.getNodeType()).thenReturn(leafSetNodeIdentifier.getNodeType());
+
+ rootDataContainerIdentifier = NodeIdentifier.create(
+ QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data"));
+ Mockito.when(rootDataContainerData.getIdentifier()).thenReturn(rootDataContainerIdentifier);
+ Mockito.when(rootDataContainerData.getNodeType()).thenReturn(rootDataContainerIdentifier.getNodeType());
+
+ // values
+ Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue);
+
+ leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData);
+ Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue);
+
+ containerNodeValue = Collections.singleton(leafSetNodeData);
+ Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue);
+
+ rootDataContainerValue = Collections.singleton(leafSetNodeData);
+ Mockito.when(rootDataContainerData.getValue()).thenReturn(rootDataContainerValue);
+ }
+
+ /**
+ * Test write {@link ContainerNode} when all its children are selected to be written by fields parameter.
+ * Depth parameter is also used and limits output to depth 1.
+ * Fields parameter has effect limiting depth parameter in the way that selected nodes and its ancestors are
+ * written regardless of their depth (some of container children have depth > 1).
+ * Fields parameter selects all container children to be written and also all children of those children.
+ */
+ @Test
+ public void writeContainerParameterPrioritiesTest() throws Exception {
+ final List<Set<QName>> limitedFields = new ArrayList<>();
+ limitedFields.add(Sets.newHashSet(leafSetNodeIdentifier.getNodeType()));
+ limitedFields.add(Sets.newHashSet(leafSetEntryNodeIdentifier.getNodeType()));
+
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, 1, limitedFields);
+
+ parameterWriter.write(containerNodeData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+ leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+ inOrder.verify(writer, Mockito.times(2)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+
+ /**
+ * Test write {@link ContainerNode} which represents data at restconf/data root.
+ * No parameters are used.
+ */
+ @Test
+ public void writeRootDataTest() throws Exception {
+ final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+ writer, null, null);
+
+ parameterWriter.write(rootDataContainerData);
+
+ final InOrder inOrder = Mockito.inOrder(writer);
+ inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+ inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+ leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+ inOrder.verify(writer, Mockito.times(1)).endNode();
+ Mockito.verifyNoMoreInteractions(writer);
+ }
+}
\ No newline at end of file
package org.opendaylight.restconf.restful.utils;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
public class ReadDataTransactionUtilTest {
- private static final TestData data = new TestData();
- private static final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = new YangInstanceIdentifier
+ private static final TestData DATA = new TestData();
+ private static final YangInstanceIdentifier.NodeIdentifier NODE_IDENTIFIER = new YangInstanceIdentifier
.NodeIdentifier(QName.create("ns", "2016-02-28", "container"));
private TransactionVarsWrapper wrapper;
@Mock
private DOMTransactionChain transactionChain;
@Mock
- private InstanceIdentifierContext<?> context;
+ private InstanceIdentifierContext<ContainerSchemaNode> context;
@Mock
private DOMDataReadOnlyTransaction read;
+ @Mock
+ private SchemaContext schemaContext;
+ @Mock
+ private ContainerSchemaNode containerSchemaNode;
+ @Mock
+ private LeafSchemaNode containerChildNode;
+ private QName containerChildQName;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- doReturn(read).when(transactionChain).newReadOnlyTransaction();
+ containerChildQName = QName.create("ns", "2016-02-28", "container-child");
+
+ when(transactionChain.newReadOnlyTransaction()).thenReturn(read);
+ when(context.getSchemaContext()).thenReturn(schemaContext);
+ when(context.getSchemaNode()).thenReturn(containerSchemaNode);
+ when(containerSchemaNode.getQName()).thenReturn(NODE_IDENTIFIER.getNodeType());
+ when(containerChildNode.getQName()).thenReturn(containerChildQName);
+ when(containerSchemaNode.getDataChildByName(containerChildQName)).thenReturn(containerChildNode);
+
wrapper = new TransactionVarsWrapper(this.context, null, this.transactionChain);
}
@Test
public void readDataConfigTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path);
- doReturn(data.path).when(context).getInstanceIdentifier();
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
+ doReturn(DATA.path).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.CONFIG;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
- assertEquals(data.data3, normalizedNode);
+ assertEquals(DATA.data3, normalizedNode);
}
@Test
public void readAllHavingOnlyConfigTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path);
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
doReturn(Futures.immediateCheckedFuture(Optional.absent())).when(read)
- .read(LogicalDatastoreType.OPERATIONAL, data.path);
- doReturn(data.path).when(context).getInstanceIdentifier();
+ .read(LogicalDatastoreType.OPERATIONAL, DATA.path);
+ doReturn(DATA.path).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
- assertEquals(data.data3, normalizedNode);
+ assertEquals(DATA.data3, normalizedNode);
}
@Test
public void readAllHavingOnlyNonConfigTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data2))).when(read)
- .read(LogicalDatastoreType.OPERATIONAL, data.path2);
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data2))).when(read)
+ .read(LogicalDatastoreType.OPERATIONAL, DATA.path2);
doReturn(Futures.immediateCheckedFuture(Optional.absent())).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path2);
- doReturn(data.path2).when(context).getInstanceIdentifier();
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path2);
+ doReturn(DATA.path2).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
- assertEquals(data.data2, normalizedNode);
+ assertEquals(DATA.data2, normalizedNode);
}
@Test
public void readDataNonConfigTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data2))).when(read)
- .read(LogicalDatastoreType.OPERATIONAL, data.path2);
- doReturn(data.path2).when(context).getInstanceIdentifier();
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data2))).when(read)
+ .read(LogicalDatastoreType.OPERATIONAL, DATA.path2);
+ doReturn(DATA.path2).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.NONCONFIG;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
- assertEquals(data.data2, normalizedNode);
+ assertEquals(DATA.data2, normalizedNode);
}
@Test
public void readContainerDataAllTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path);
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data4))).when(read)
- .read(LogicalDatastoreType.OPERATIONAL, data.path);
- doReturn(data.path).when(context).getInstanceIdentifier();
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data4))).when(read)
+ .read(LogicalDatastoreType.OPERATIONAL, DATA.path);
+ doReturn(DATA.path).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
final ContainerNode checkingData = Builders
.containerBuilder()
- .withNodeIdentifier(nodeIdentifier)
- .withChild(data.contentLeaf)
- .withChild(data.contentLeaf2)
+ .withNodeIdentifier(NODE_IDENTIFIER)
+ .withChild(DATA.contentLeaf)
+ .withChild(DATA.contentLeaf2)
.build();
assertEquals(checkingData, normalizedNode);
}
@Test
public void readContainerDataConfigNoValueOfContentTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path);
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.data4))).when(read)
- .read(LogicalDatastoreType.OPERATIONAL, data.path);
- doReturn(data.path).when(context).getInstanceIdentifier();
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data4))).when(read)
+ .read(LogicalDatastoreType.OPERATIONAL, DATA.path);
+ doReturn(DATA.path).when(context).getInstanceIdentifier();
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(
RestconfDataServiceConstant.ReadData.ALL, wrapper);
final ContainerNode checkingData = Builders
.containerBuilder()
- .withNodeIdentifier(nodeIdentifier)
- .withChild(data.contentLeaf)
- .withChild(data.contentLeaf2)
+ .withNodeIdentifier(NODE_IDENTIFIER)
+ .withChild(DATA.contentLeaf)
+ .withChild(DATA.contentLeaf2)
.build();
assertEquals(checkingData, normalizedNode);
}
@Test
public void readListDataAllTest() {
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.listData))).when(read)
- .read(LogicalDatastoreType.OPERATIONAL, data.path3);
- doReturn(Futures.immediateCheckedFuture(Optional.of(data.listData2))).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path3);
- doReturn(data.path3).when(context).getInstanceIdentifier();
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.listData))).when(read)
+ .read(LogicalDatastoreType.OPERATIONAL, DATA.path3);
+ doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.listData2))).when(read)
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path3);
+ doReturn(DATA.path3).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
final MapNode checkingData = Builders
.mapBuilder()
.withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(QName.create("ns", "2016-02-28", "list")))
- .withChild(data.checkData)
+ .withChild(DATA.checkData)
.build();
assertEquals(checkingData, normalizedNode);
}
@Test
public void readDataWrongPathOrNoContentTest() {
doReturn(Futures.immediateCheckedFuture(Optional.absent())).when(read)
- .read(LogicalDatastoreType.CONFIGURATION, data.path2);
- doReturn(data.path2).when(context).getInstanceIdentifier();
+ .read(LogicalDatastoreType.CONFIGURATION, DATA.path2);
+ doReturn(DATA.path2).when(context).getInstanceIdentifier();
final String valueOfContent = RestconfDataServiceConstant.ReadData.CONFIG;
final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
assertNull(normalizedNode);
@Test(expected = RestconfDocumentedException.class)
public void readDataFailTest() {
final String valueOfContent = RestconfDataServiceConstant.ReadData.READ_TYPE_TX;
- final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, null);
+ final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(
+ valueOfContent, wrapper);
assertNull(normalizedNode);
}
// no parameters, default values should be used
when(uriInfo.getQueryParameters()).thenReturn(parameters);
- final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
assertEquals("Not correctly parsed URI parameter",
RestconfDataServiceConstant.ReadData.ALL, parsedParameters.getContent());
- assertFalse("Not correctly parsed URI parameter",
- parsedParameters.getDepth().isPresent());
+ assertNull("Not correctly parsed URI parameter",
+ parsedParameters.getDepth());
+ assertNull("Not correctly parsed URI parameter",
+ parsedParameters.getFields());
}
/**
final String content = "config";
final String depth = "10";
+ final String fields = containerChildQName.getLocalName();
parameters.put("content", Collections.singletonList(content));
parameters.put("depth", Collections.singletonList(depth));
+ parameters.put("fields", Collections.singletonList(fields));
when(uriInfo.getQueryParameters()).thenReturn(parameters);
- final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
+ // content
assertEquals("Not correctly parsed URI parameter",
content, parsedParameters.getContent());
- assertTrue("Not correctly parsed URI parameter",
- parsedParameters.getDepth().isPresent());
+
+ // depth
+ assertNotNull("Not correctly parsed URI parameter",
+ parsedParameters.getDepth());
+ assertEquals("Not correctly parsed URI parameter",
+ depth, parsedParameters.getDepth().toString());
+
+ // fields
+ assertNotNull("Not correctly parsed URI parameter",
+ parsedParameters.getFields());
+ assertEquals("Not correctly parsed URI parameter",
+ 1, parsedParameters.getFields().size());
+ assertEquals("Not correctly parsed URI parameter",
+ 1, parsedParameters.getFields().get(0).size());
assertEquals("Not correctly parsed URI parameter",
- depth, parsedParameters.getDepth().get().toString());
+ containerChildQName, parsedParameters.getFields().get(0).iterator().next());
}
/**
when(uriInfo.getQueryParameters()).thenReturn(parameters);
try {
- ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
fail("Test expected to fail due to not allowed parameter value");
} catch (final RestconfDocumentedException e) {
// Bad request
when(uriInfo.getQueryParameters()).thenReturn(parameters);
try {
- ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
fail("Test expected to fail due to not allowed parameter value");
} catch (final RestconfDocumentedException e) {
// Bad request
when(uriInfo.getQueryParameters()).thenReturn(parameters);
try {
- ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
fail("Test expected to fail due to not allowed parameter value");
} catch (final RestconfDocumentedException e) {
// Bad request
when(uriInfo.getQueryParameters()).thenReturn(parameters);
try {
- ReadDataTransactionUtil.parseUriParameters(uriInfo);
+ ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
fail("Test expected to fail due to not allowed parameter value");
} catch (final RestconfDocumentedException e) {
// Bad request
--- /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.utils.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.controller.md.sal.rest.common.TestRestconfUtils;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+/**
+ * Unit test for {@link ParserFieldsParameter}
+ */
+public class ParserFieldsParameterTest {
+
+ @Mock
+ private InstanceIdentifierContext<ContainerSchemaNode> identifierContext;
+
+ // container jukebox
+ @Mock
+ private ContainerSchemaNode containerJukebox;
+ private QName jukeboxQName;
+
+ // container player
+ @Mock
+ private ContainerSchemaNode containerPlayer;
+ private QName playerQName;
+
+ // container library
+ @Mock
+ private ContainerSchemaNode containerLibrary;
+ private QName libraryQName;
+
+ // container augmented library
+ @Mock
+ private ContainerSchemaNode augmentedContainerLibrary;
+ private QName augmentedLibraryQName;
+
+ // list album
+ @Mock
+ private ListSchemaNode listAlbum;
+ private QName albumQName;
+
+ // leaf name
+ @Mock
+ private LeafSchemaNode leafName;
+ private QName nameQName;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ final SchemaContext schemaContext = TestRestconfUtils.loadSchemaContext("/jukebox");
+
+ final QNameModule qNameModule = QNameModule.create(URI.create("http://example.com/ns/example-jukebox"),
+ new SimpleDateFormat("yyyy-MM-dd").parse("2015-04-04"));
+
+ jukeboxQName = QName.create(qNameModule, "jukebox");
+ playerQName = QName.create(qNameModule, "player");
+ libraryQName = QName.create(qNameModule, "library");
+ augmentedLibraryQName = QName.create(
+ QNameModule.create(
+ URI.create("http://example.com/ns/augmented-jukebox"),
+ new SimpleDateFormat("yyyy-MM-dd").parse("2016-05-05")),
+ "augmented-library");
+ albumQName = QName.create(qNameModule, "album");
+ nameQName = QName.create(qNameModule, "name");
+
+ Mockito.when(identifierContext.getSchemaContext()).thenReturn(schemaContext);
+ Mockito.when(containerJukebox.getQName()).thenReturn(jukeboxQName);
+ Mockito.when(identifierContext.getSchemaNode()).thenReturn(containerJukebox);
+
+ Mockito.when(containerLibrary.getQName()).thenReturn(libraryQName);
+ Mockito.when(containerJukebox.getDataChildByName(libraryQName)).thenReturn(containerLibrary);
+
+ Mockito.when(augmentedContainerLibrary.getQName()).thenReturn(augmentedLibraryQName);
+ Mockito.when(containerJukebox.getDataChildByName(augmentedLibraryQName)).thenReturn(augmentedContainerLibrary);
+
+ Mockito.when(containerPlayer.getQName()).thenReturn(playerQName);
+ Mockito.when(containerJukebox.getDataChildByName(playerQName)).thenReturn(containerPlayer);
+
+ Mockito.when(listAlbum.getQName()).thenReturn(albumQName);
+ Mockito.when(containerLibrary.getDataChildByName(albumQName)).thenReturn(listAlbum);
+
+ Mockito.when(leafName.getQName()).thenReturn(nameQName);
+ Mockito.when(listAlbum.getDataChildByName(nameQName)).thenReturn(leafName);
+ }
+
+ /**
+ * Test parse fields parameter containing only one child selected
+ */
+ @Test
+ public void parseFieldsParameterSimplePathTest() {
+ final String input = "library";
+ final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(1, parsedFields.size());
+ assertEquals(1, parsedFields.get(0).size());
+ assertTrue(parsedFields.get(0).contains(libraryQName));
+ }
+
+ /**
+ * Test parse fields parameter containing two child nodes selected
+ */
+ @Test
+ public void parseFieldsParameterDoublePathTest() {
+ final String input = "library;player";
+ final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(1, parsedFields.size());
+ assertEquals(2, parsedFields.get(0).size());
+ assertTrue(parsedFields.get(0).contains(libraryQName));
+ assertTrue(parsedFields.get(0).contains(playerQName));
+ }
+
+ /**
+ * Test parse fields parameter containing sub-children selected delimited by slash
+ */
+ @Test
+ public void parseFieldsParameterSubPathTest() {
+ final String input = "library/album/name";
+ final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(3, parsedFields.size());
+
+ assertEquals(1, parsedFields.get(0).size());
+ assertTrue(parsedFields.get(0).contains(libraryQName));
+
+ assertEquals(1, parsedFields.get(1).size());
+ assertTrue(parsedFields.get(1).contains(albumQName));
+
+ assertEquals(1, parsedFields.get(2).size());
+ assertTrue(parsedFields.get(2).contains(nameQName));
+ }
+
+ /**
+ * Test parse fields parameter containing sub-children selected delimited by parenthesis
+ */
+ @Test
+ public void parseFieldsParameterChildrenPathTest() {
+ final String input = "library(album(name))";
+ final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(3, parsedFields.size());
+
+ assertEquals(1, parsedFields.get(0).size());
+ assertTrue(parsedFields.get(0).contains(libraryQName));
+
+ assertEquals(1, parsedFields.get(1).size());
+ assertTrue(parsedFields.get(1).contains(albumQName));
+
+ assertEquals(1, parsedFields.get(2).size());
+ assertTrue(parsedFields.get(2).contains(nameQName));
+ }
+
+ /**
+ * Test parse fields parameter when augmentation with different namespace is used
+ */
+ @Test
+ public void parseFieldsParameterNamespaceTest() {
+ final String input = "augmented-jukebox:augmented-library";
+ final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(1, parsedFields.size());
+
+ assertEquals(1, parsedFields.get(0).size());
+ assertTrue(parsedFields.get(0).contains(augmentedLibraryQName));
+ }
+
+ /**
+ * Test parse fields parameter containing not expected character
+ */
+ @Test
+ public void parseFieldsParameterNotExpectedCharacterNegativeTest() {
+ final String input = "*";
+
+ try {
+ ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+ fail("Test should fail due to not expected character used in parameter input value");
+ } catch (final RestconfDocumentedException e) {
+ // Bad request
+ assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+ assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+ assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+ }
+ }
+
+ /**
+ * Test parse fields parameter with missing closing parenthesis
+ */
+ @Test
+ public void parseFieldsParameterMissingParenthesisNegativeTest() {
+ final String input = "library(";
+
+ try {
+ ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+ fail("Test should fail due to missing closing parenthesis");
+ } catch (final RestconfDocumentedException e) {
+ // Bad request
+ assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+ assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+ assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+ }
+ }
+
+ /**
+ * Test parse fields parameter when not existing child node selected
+ */
+ @Test
+ public void parseFieldsParameterMissingChildNodeNegativeTest() {
+ final String input = "library(not-existing)";
+
+ try {
+ ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+ fail("Test should fail due to missing child node in parent node");
+ } catch (final RestconfDocumentedException e) {
+ // Bad request
+ assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+ assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+ assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+ }
+ }
+
+ /**
+ * Test parse fields parameter with unexpected character after parenthesis
+ */
+ @Test
+ public void parseFieldsParameterAfterParenthesisNegativeTest() {
+ final String input = "library(album);";
+
+ try {
+ ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+ fail("Test should fail due to unexpected character after parenthesis");
+ } catch (final RestconfDocumentedException e) {
+ // Bad request
+ assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+ assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+ assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+ }
+ }
+
+ /**
+ * Test parse fields parameter with missing semicolon after parenthesis
+ */
+ @Test
+ public void parseFieldsParameterMissingSemicolonNegativeTest() {
+ final String input = "library(album)player";
+
+ try {
+ ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+ fail("Test should fail due to missing semicolon after parenthesis");
+ } catch (final RestconfDocumentedException e) {
+ // Bad request
+ assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+ assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+ assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+module augmented-jukebox {
+
+ namespace "http://example.com/ns/augmented-jukebox";
+ prefix "augmented-jbox";
+
+ revision "2016-05-05" {
+ description "Initial version.";
+ }
+
+ import example-jukebox {prefix jbox; revision-date "2015-04-04";}
+
+ augment "/jbox:jukebox" {
+ container augmented-library {
+ }
+ }
+ }
\ No newline at end of file