From cdea09e3a7291fe759a992671b2c6c2dac52cd43 Mon Sep 17 00:00:00 2001 From: Martin Ciglan Date: Sun, 13 Mar 2016 18:29:21 +0100 Subject: [PATCH] Bug 3866: Support for Restconf HTTP Patch - JSON body reader/writer in place - XML body reader/writer in place - refactored BrokerFacade and RestconfImpl code to apply HTTP Patch within transaction in order to be able to rollback in case of error(s) - review comments implemented Change-Id: Ib66b6d0e49fc32709f1163e517240ec0bddd0a60 Signed-off-by: Martin Ciglan --- .../netconf/sal/rest/api/RestconfService.java | 15 ++ .../sal/rest/impl/JsonToPATCHBodyReader.java | 240 ++++++++++++++++++ .../sal/rest/impl/PATCHJsonBodyWriter.java | 111 ++++++++ .../sal/rest/impl/PATCHXmlBodyWriter.java | 125 +++++++++ .../sal/rest/impl/RestconfApplication.java | 4 + .../rest/impl/RestconfCompositeWrapper.java | 12 + .../sal/rest/impl/XmlToPATCHBodyReader.java | 163 ++++++++++++ .../sal/restconf/impl/BrokerFacade.java | 136 +++++++++- .../netconf/sal/restconf/impl/PATCH.java | 23 ++ .../sal/restconf/impl/PATCHContext.java | 39 +++ .../sal/restconf/impl/PATCHEditOperation.java | 26 ++ .../sal/restconf/impl/PATCHEntity.java | 46 ++++ .../sal/restconf/impl/PATCHStatusContext.java | 43 ++++ .../sal/restconf/impl/PATCHStatusEntity.java | 36 +++ .../sal/restconf/impl/RestconfImpl.java | 19 +- .../StatisticsRestconfServiceWrapper.java | 11 + .../instance-identifier-patch-module.yang | 50 ++++ .../md/sal/rest/common/TestRestconfUtils.java | 2 +- .../providers/AbstractBodyReaderTest.java | 10 +- .../providers/TestJsonPATCHBodyReader.java | 54 ++++ .../providers/TestXmlPATCHBodyReader.java | 50 ++++ .../json/jsonPATCHdata.json | 34 +++ .../instanceidentifier/xml/xmlPATCHdata.xml | 28 ++ .../instance-identifier-patch-module.yang | 50 ++++ 24 files changed, 1316 insertions(+), 11 deletions(-) create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPATCHBodyReader.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHJsonBodyWriter.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHXmlBodyWriter.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPATCHBodyReader.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCH.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHContext.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEditOperation.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEntity.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusContext.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusEntity.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/main/yang/instance-identifier-patch-module.yang create mode 100644 opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReader.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java create mode 100644 opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdata.json create mode 100644 opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdata.xml create mode 100644 opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/yang/instance-identifier-patch-module.yang diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java index fac2b1872e..178476fe74 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java @@ -21,7 +21,10 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import org.opendaylight.netconf.sal.rest.impl.PATCH; import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext; /** * The URI hierarchy for the RESTCONF resources consists of an entry point container, 4 top-level resources, and 1 @@ -151,4 +154,16 @@ public interface RestconfService { MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public NormalizedNodeContext getAvailableStreams(@Context UriInfo uriInfo); + @PATCH + @Path("/config/{identifier:.+}") + @Consumes({ Draft02.MediaTypes.PATCH + JSON, Draft02.MediaTypes.PATCH + XML}) + @Produces({ Draft02.MediaTypes.PATCH_STATUS + JSON, Draft02.MediaTypes.PATCH_STATUS + XML}) + PATCHStatusContext patchConfigurationData(@Encoded @PathParam("identifier") String identifier, PATCHContext + context, @Context UriInfo uriInfo); + + @PATCH + @Path("/config") + @Consumes({ Draft02.MediaTypes.PATCH + JSON, Draft02.MediaTypes.PATCH + XML}) + @Produces({ Draft02.MediaTypes.PATCH_STATUS + JSON, Draft02.MediaTypes.PATCH_STATUS + XML}) + PATCHStatusContext patchConfigurationData(PATCHContext context, @Context UriInfo uriInfo); } diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPATCHBodyReader.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPATCHBodyReader.java new file mode 100644 index 0000000000..6d3f7ea34f --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPATCHBodyReader.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.gson.stream.JsonReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity; +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.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException; +import org.opendaylight.yangtools.yang.data.util.AbstractStringInstanceIdentifierCodec; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Provider +@Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON}) +public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider implements MessageBodyReader { + + private final static Logger LOG = LoggerFactory.getLogger(JsonToPATCHBodyReader.class); + private String patchId; + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return true; + } + + @Override + public PATCHContext readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + try { + return readFrom(getInstanceIdentifierContext(), entityStream); + } catch (final Exception e) { + throw propagateExceptionAs(e); + } + } + + private static RuntimeException propagateExceptionAs(Exception e) throws RestconfDocumentedException { + if(e instanceof RestconfDocumentedException) { + throw (RestconfDocumentedException)e; + } + + if(e instanceof ResultAlreadySetException) { + LOG.debug("Error parsing json input:", e); + + throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. "); + } + + throw new RestconfDocumentedException("Error parsing json input: " + e.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, e); + } + + public PATCHContext readFrom(final String uriPath, final InputStream entityStream) throws + RestconfDocumentedException { + try { + return readFrom(ControllerContext.getInstance().toInstanceIdentifier(uriPath), entityStream); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + private PATCHContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream) throws IOException { + if (entityStream.available() < 1) { + return new PATCHContext(path, null, null); + } + + final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream)); + final List resultList = read(jsonReader, path); + jsonReader.close(); + + return new PATCHContext(path, resultList, patchId); + } + + private List read(final JsonReader in, InstanceIdentifierContext path) throws + IOException { + + boolean inEdit = false; + boolean inValue = false; + String operation = null; + String target = null; + String editId = null; + List resultCollection = new ArrayList<>(); + + while (in.hasNext()) { + switch (in.peek()) { + case STRING: + case NUMBER: + in.nextString(); + break; + case BOOLEAN: + Boolean.toString(in.nextBoolean()); + break; + case NULL: + in.nextNull(); + break; + case BEGIN_ARRAY: + in.beginArray(); + break; + case BEGIN_OBJECT: + if (inEdit && operation != null & target != null & inValue) { + //let's do the stuff - find out target node +// StringInstanceIdentifierCodec codec = new StringInstanceIdentifierCodec(path +// .getSchemaContext()); +// if (path.getInstanceIdentifier().toString().equals("/")) { +// final YangInstanceIdentifier deserialized = codec.deserialize(target); +// } + DataSchemaNode targetNode = ((DataNodeContainer)(path.getSchemaNode())).getDataChildByName + (target.replace("/", "")); + if (targetNode == null) { + LOG.debug("Target node {} not found in path {} ", target, path.getSchemaNode()); + throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE); + } else { + + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + + //keep on parsing json from place where target points + final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(), + path.getSchemaNode()); + jsonParser.parse(in); + + final YangInstanceIdentifier targetII = path.getInstanceIdentifier().node(targetNode.getQName()); + resultCollection.add(new PATCHEntity(editId, operation, targetII, resultHolder.getResult + ())); + inValue = false; + + operation = null; + target = null; + } + in.endObject(); + } else { + in.beginObject(); + } + break; + case END_DOCUMENT: + break; + case NAME: + final String name = in.nextName(); + + switch (name) { + case "edit" : inEdit = true; + break; + case "operation" : operation = in.nextString(); + break; + case "target" : target = in.nextString(); + break; + case "value" : inValue = true; + break; + case "patch-id" : patchId = in.nextString(); + break; + case "edit-id" : editId = in.nextString(); + break; + } + break; + case END_OBJECT: + in.endObject(); + break; + case END_ARRAY: + in.endArray(); + break; + + default: + break; + } + } + + return ImmutableList.copyOf(resultCollection); + } + + private class StringInstanceIdentifierCodec extends AbstractStringInstanceIdentifierCodec { + + private final DataSchemaContextTree dataContextTree; + private final SchemaContext context; + + StringInstanceIdentifierCodec(SchemaContext context) { + this.context = Preconditions.checkNotNull(context); + this.dataContextTree = DataSchemaContextTree.from(context); + } + + @Nonnull + @Override + protected DataSchemaContextTree getDataContextTree() { + return dataContextTree; + } + + @Nullable + @Override + protected String prefixForNamespace(@Nonnull URI namespace) { + final Module module = context.findModuleByNamespaceAndRevision(namespace, null); + return module == null ? null : module.getName(); + } + + @Nullable + @Override + protected QName createQName(@Nonnull String prefix, @Nonnull String localName) { + throw new UnsupportedOperationException("Not implemented"); + } + + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHJsonBodyWriter.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHJsonBodyWriter.java new file mode 100644 index 0000000000..9f9ab94933 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHJsonBodyWriter.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.common.base.Charsets; +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.util.List; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import org.opendaylight.netconf.sal.rest.api.Draft02.MediaTypes; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusEntity; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory; + +@Provider +@Produces({MediaTypes.PATCH_STATUS + RestconfService.JSON}) +public class PATCHJsonBodyWriter implements MessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type.equals(PATCHStatusContext.class); + } + + @Override + public long getSize(PATCHStatusContext patchStatusContext, Class type, Type genericType, Annotation[] + annotations, MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(PATCHStatusContext patchStatusContext, Class type, Type genericType, Annotation[] + annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + + final JsonWriter jsonWriter = createJsonWriter(entityStream); + jsonWriter.beginObject().name("ietf-yang-patch:yang-patch-status"); + jsonWriter.beginObject(); + jsonWriter.name("patch-id").value(patchStatusContext.getPatchId()); + if (patchStatusContext.isOk()) { + jsonWriter.name("ok").nullValue(); + } else { + if (patchStatusContext.getGlobalErrors() != null) { + reportErrors(patchStatusContext.getGlobalErrors(), jsonWriter); + } + + jsonWriter.name("edit-status"); + jsonWriter.beginObject(); + jsonWriter.name("edit"); + jsonWriter.beginArray(); + for (PATCHStatusEntity patchStatusEntity : patchStatusContext.getEditCollection()) { + jsonWriter.beginObject(); + jsonWriter.name("edit-id").value(patchStatusEntity.getEditId()); + if (patchStatusEntity.getEditErrors() != null) { + reportErrors(patchStatusEntity.getEditErrors(), jsonWriter); + } else { + if (patchStatusEntity.isOk()) { + jsonWriter.name("ok").nullValue(); + } + } + jsonWriter.endObject(); + } + jsonWriter.endArray(); + jsonWriter.endObject(); + } + jsonWriter.endObject(); + jsonWriter.endObject(); + jsonWriter.flush(); + + } + + private static void reportErrors(List errors, JsonWriter jsonWriter) throws IOException { + jsonWriter.name("errors"); + jsonWriter.beginObject(); + jsonWriter.name("error"); + jsonWriter.beginArray(); + + for (RestconfError restconfError : errors) { + jsonWriter.beginObject(); + jsonWriter.name("error-type").value(restconfError.getErrorType().getErrorTypeTag()); + jsonWriter.name("error-tag").value(restconfError.getErrorTag().getTagValue()); + //TODO: fix error-path reporting (separate error-path from error-message) + //jsonWriter.name("error-path").value(restconfError.getErrorPath()); + jsonWriter.name("error-message").value(restconfError.getErrorMessage()); + jsonWriter.endObject(); + } + + jsonWriter.endArray(); + jsonWriter.endObject(); + } + + private static JsonWriter createJsonWriter(final OutputStream entityStream) { + return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, Charsets.UTF_8)); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHXmlBodyWriter.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHXmlBodyWriter.java new file mode 100644 index 0000000000..2d094a6439 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHXmlBodyWriter.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.rest.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.opendaylight.netconf.sal.rest.api.Draft02.MediaTypes; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusEntity; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError; + +@Provider +@Produces({ MediaTypes.PATCH_STATUS + RestconfService.XML}) +public class PATCHXmlBodyWriter implements MessageBodyWriter { + + private static final XMLOutputFactory XML_FACTORY; + + static { + XML_FACTORY = XMLOutputFactory.newFactory(); + XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type.equals(PATCHStatusContext.class); + } + + @Override + public long getSize(PATCHStatusContext patchStatusContext, Class type, Type genericType, Annotation[] + annotations, MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(PATCHStatusContext patchStatusContext, Class type, Type genericType, Annotation[] + annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + + try { + XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(entityStream); + writeDocument(xmlWriter, patchStatusContext); + } catch (final XMLStreamException e) { + throw new IllegalStateException(e); + } catch (final FactoryConfigurationError e) { + throw new IllegalStateException(e); + } + + } + + private static void writeDocument(XMLStreamWriter writer, PATCHStatusContext context) throws XMLStreamException, IOException { + writer.writeStartElement("", "yang-patch-status", "urn:ietf:params:xml:ns:yang:ietf-yang-patch"); + writer.writeStartElement("patch-id"); + writer.writeCharacters(context.getPatchId()); + writer.writeEndElement(); + + if (context.isOk()) { + writer.writeEmptyElement("ok"); + } else { + if (context.getGlobalErrors() != null) { + reportErrors(context.getGlobalErrors(), writer); + } + writer.writeStartElement("edit-status"); + for (PATCHStatusEntity patchStatusEntity : context.getEditCollection()) { + writer.writeStartElement("edit"); + writer.writeStartElement("edit-id"); + writer.writeCharacters(patchStatusEntity.getEditId()); + writer.writeEndElement(); + if (patchStatusEntity.getEditErrors() != null) { + reportErrors(patchStatusEntity.getEditErrors(), writer); + } else { + if (patchStatusEntity.isOk()) { + writer.writeEmptyElement("ok"); + } + } + writer.writeEndElement(); + } + writer.writeEndElement(); + + } + writer.writeEndElement(); + + writer.flush(); + } + + private static void reportErrors(List errors, XMLStreamWriter writer) throws IOException, XMLStreamException { + writer.writeStartElement("errors"); + + for (RestconfError restconfError : errors) { + writer.writeStartElement("error-type"); + writer.writeCharacters(restconfError.getErrorType().getErrorTypeTag()); + writer.writeEndElement(); + writer.writeStartElement("error-tag"); + writer.writeCharacters(restconfError.getErrorTag().getTagValue()); + writer.writeEndElement(); + //TODO: fix error-path reporting (separate error-path from error-message) +// writer.writeStartElement("error-path"); +// writer.writeCharacters(restconfError.getErrorPath()); +// writer.writeEndElement(); + writer.writeStartElement("error-message"); + writer.writeCharacters(restconfError.getErrorMessage()); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java index bfcd826f5d..46b8743832 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java @@ -27,6 +27,10 @@ public class RestconfApplication extends Application { .add(RestconfDocumentedExceptionMapper.class) .add(XmlNormalizedNodeBodyReader.class) .add(JsonNormalizedNodeBodyReader.class) + .add(JsonToPATCHBodyReader.class) + .add(XmlToPATCHBodyReader.class) + .add(PATCHJsonBodyWriter.class) + .add(PATCHXmlBodyWriter.class) .add(NormalizedNodeJsonBodyWriter.class) .add(NormalizedNodeXmlBodyWriter.class) .add(SchemaExportContentYinBodyWriter.class) diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java index 4015d9ac22..f184df0d05 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java @@ -15,6 +15,8 @@ import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContext; import org.opendaylight.netconf.md.sal.rest.schema.SchemaRetrievalService; import org.opendaylight.netconf.sal.rest.api.RestconfService; import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext; public class RestconfCompositeWrapper implements RestconfService, SchemaRetrievalService { @@ -107,6 +109,16 @@ public class RestconfCompositeWrapper implements RestconfService, SchemaRetrieva return restconf.getAvailableStreams(uriInfo); } + @Override + public PATCHStatusContext patchConfigurationData(final String identifier, PATCHContext payload, UriInfo uriInfo) { + return restconf.patchConfigurationData(identifier, payload, uriInfo); + } + + @Override + public PATCHStatusContext patchConfigurationData(final PATCHContext context, final UriInfo uriInfo) { + return restconf.patchConfigurationData(context, uriInfo); + } + @Override public SchemaExportContext getSchema(final String mountId) { return schema.getSchema(mountId); diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPATCHBodyReader.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPATCHBodyReader.java new file mode 100644 index 0000000000..fe63f31373 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPATCHBodyReader.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.opendaylight.netconf.sal.rest.api.Draft02.MediaTypes; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +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.data.impl.codec.xml.XmlUtils; +import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; + +@Provider +@Consumes({MediaTypes.PATCH + RestconfService.XML}) +public class XmlToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider implements + MessageBodyReader { + + private final static Logger LOG = LoggerFactory.getLogger(XmlToPATCHBodyReader.class); + private static final DocumentBuilderFactory BUILDERFACTORY; + + static { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + try { + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + } catch (final ParserConfigurationException e) { + throw new ExceptionInInitializerError(e); + } + factory.setNamespaceAware(true); + factory.setCoalescing(true); + factory.setIgnoringElementContentWhitespace(true); + factory.setIgnoringComments(true); + BUILDERFACTORY = factory; + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return true; + } + + @Override + public PATCHContext readFrom(Class type, Type genericType, Annotation[] annotations, MediaType + mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, + WebApplicationException { + + try { + final InstanceIdentifierContext path = getInstanceIdentifierContext(); + + if (entityStream.available() < 1) { + // represent empty nopayload input + return new PATCHContext(path, null, null); + } + + final DocumentBuilder dBuilder; + try { + dBuilder = BUILDERFACTORY.newDocumentBuilder(); + } catch (final ParserConfigurationException e) { + throw new IllegalStateException("Failed to parse XML document", e); + } + final Document doc = dBuilder.parse(entityStream); + + return parse(path, doc); + } catch (final RestconfDocumentedException e) { + throw e; + } catch (final Exception e) { + LOG.debug("Error parsing xml input", e); + + throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE); + } + } + + private PATCHContext parse(final InstanceIdentifierContext pathContext, final Document doc) { + final List resultCollection = new ArrayList<>(); + final String patchId = doc.getElementsByTagName("patch-id").item(0).getFirstChild().getNodeValue(); + final NodeList editNodes = doc.getElementsByTagName("edit"); + final DataSchemaNode schemaNode = (DataSchemaNode) pathContext.getSchemaNode(); + final DomToNormalizedNodeParserFactory parserFactory = + DomToNormalizedNodeParserFactory.getInstance(XmlUtils.DEFAULT_XML_CODEC_PROVIDER, + pathContext.getSchemaContext()); + + for (int i = 0; i < editNodes.getLength(); i++) { + Element element = (Element) editNodes.item(i); + final String operation = element.getElementsByTagName("operation").item(0).getFirstChild().getNodeValue(); + final String editId = element.getElementsByTagName("edit-id").item(0).getFirstChild().getNodeValue(); + final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue(); + DataSchemaNode targetNode = ((DataNodeContainer)(pathContext.getSchemaNode())).getDataChildByName + (target.replace("/", "")); + if (targetNode == null) { + LOG.debug("Target node {} not found in path {} ", target, pathContext.getSchemaNode()); + throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE); + } else { + final YangInstanceIdentifier targetII = pathContext.getInstanceIdentifier().node(targetNode.getQName()); + final NodeList valueNodes = element.getElementsByTagName("value").item(0).getChildNodes(); + Element value = null; + for (int j = 0; j < valueNodes.getLength(); j++) { + if (valueNodes.item(j) instanceof Element) { + value = (Element) valueNodes.item(j); + break; + } + } + NormalizedNode parsed = null; + if (schemaNode instanceof ContainerSchemaNode) { + parsed = parserFactory.getContainerNodeParser().parse(Collections.singletonList(value), + (ContainerSchemaNode) targetNode); + } else if (schemaNode instanceof ListSchemaNode) { + NormalizedNode parsedValue = parserFactory.getMapEntryNodeParser().parse(Collections + .singletonList(value), (ListSchemaNode) targetNode); + parsed = ImmutableNodes.mapNodeBuilder().withNodeIdentifier(new NodeIdentifier + (targetNode.getQName())).withChild((MapEntryNode) parsedValue).build(); + } + + resultCollection.add(new PATCHEntity(editId, operation, targetII, parsed)); + } + } + + return new PATCHContext(pathContext, ImmutableList.copyOf(resultCollection), patchId); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java index 0a5409cb10..b4be7c683c 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the @@ -11,6 +11,7 @@ import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastor import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -126,6 +127,88 @@ public class BrokerFacade { throw new RestconfDocumentedException(errMsg); } + public PATCHStatusContext patchConfigurationDataWithinTransaction(final PATCHContext context, + final SchemaContext globalSchema) { + final DOMDataReadWriteTransaction patchTransaction = domDataBroker.newReadWriteTransaction(); + List editCollection = new ArrayList<>(); + List editErrors; + List globalErrors = null; + int errorCounter = 0; + + for (PATCHEntity patchEntity : context.getData()) { + final PATCHEditOperation operation = PATCHEditOperation.valueOf(patchEntity.getOperation().toUpperCase()); + + switch (operation) { + case CREATE: + if (errorCounter == 0) { + try { + postDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(), + patchEntity.getNode(), globalSchema); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); + } catch (RestconfDocumentedException e) { + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); + errorCounter++; + } + } + break; + case REPLACE: + if (errorCounter == 0) { + try { + putDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity + .getTargetNode(), patchEntity.getNode(), globalSchema); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); + } catch (RestconfDocumentedException e) { + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); + errorCounter++; + } + } + break; + case DELETE: + if (errorCounter == 0) { + try { + deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity + .getTargetNode()); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); + } catch (RestconfDocumentedException e) { + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors)); + errorCounter++; + } + } + break; + case REMOVE: + if (errorCounter == 0) { + try { + deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity + .getTargetNode()); + editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null)); + } catch (RestconfDocumentedException e) { + LOG.error("Error removing {} by {} operation", patchEntity.getTargetNode().toString(), + patchEntity.getEditId(), e); + } + } + break; + } + } + + //TODO: make sure possible global errors are filled up correctly and decide transaction submission based on that + //globalErrors = new ArrayList<>(); + if (errorCounter == 0) { + final CheckedFuture submit = patchTransaction.submit(); + return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), true, + globalErrors); + } else { + patchTransaction.cancel(); + return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), false, + globalErrors); + } + } + // POST configuration public CheckedFuture commitConfigurationDataPost( final SchemaContext globalSchema, final YangInstanceIdentifier path, final NormalizedNode payload) { @@ -190,7 +273,7 @@ public class BrokerFacade { private NormalizedNode readDataViaTransaction(final DOMDataReadTransaction transaction, final LogicalDatastoreType datastore, final YangInstanceIdentifier path) { - LOG.trace("Read " + datastore.name() + " via Restconf: {}", path); + LOG.trace("Read {} via Restconf: {}", datastore.name(), path); final ListenableFuture>> listenableFuture = transaction.read(datastore, path); if (listenableFuture != null) { Optional> optional; @@ -198,7 +281,7 @@ public class BrokerFacade { LOG.debug("Reading result data from transaction."); optional = listenableFuture.get(); } catch (InterruptedException | ExecutionException e) { - LOG.warn("Exception by reading " + datastore.name() + " via Restconf: {}", path, e); + LOG.warn("Exception by reading {} via Restconf: {}", datastore.name(), path, e); throw new RestconfDocumentedException("Problem to get data from transaction.", e.getCause()); } @@ -218,7 +301,7 @@ public class BrokerFacade { // not sure if this will work for choice case DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction(); if(payload instanceof MapNode) { - LOG.trace("POST " + datastore.name() + " via Restconf: {} with payload {}", path, payload); + LOG.trace("POST {} via Restconf: {} with payload {}", datastore.name(), path, payload); final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path); try { transaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); @@ -249,18 +332,40 @@ public class BrokerFacade { return transaction.submit(); } + private void postDataWithinTransaction( + final DOMDataReadWriteTransaction rWTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, final SchemaContext schemaContext) { + // FIXME: This is doing correct post for container and list children + // not sure if this will work for choice case + if(payload instanceof MapNode) { + LOG.trace("POST {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload); + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path); + rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + ensureParentsByMerge(datastore, path, rWTransaction, schemaContext); + for(final MapEntryNode child : ((MapNode) payload).getValue()) { + final YangInstanceIdentifier childPath = path.node(child.getIdentifier()); + checkItemDoesNotExists(rWTransaction, datastore, childPath); + rWTransaction.put(datastore, childPath, child); + } + } else { + checkItemDoesNotExists(rWTransaction,datastore, path); + ensureParentsByMerge(datastore, path, rWTransaction, schemaContext); + rWTransaction.put(datastore, path, payload); + } + } + private void checkItemDoesNotExists(final DOMDataReadWriteTransaction rWTransaction,final LogicalDatastoreType store, final YangInstanceIdentifier path) { final ListenableFuture futureDatastoreData = rWTransaction.exists(store, path); try { if (futureDatastoreData.get()) { final String errMsg = "Post Configuration via Restconf was not executed because data already exists"; - LOG.trace(errMsg + ":{}", path); + LOG.trace("{}:{}", errMsg, path); rWTransaction.cancel(); throw new RestconfDocumentedException("Data already exists for path: " + path, ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS); } } catch (InterruptedException | ExecutionException e) { - LOG.warn("It wasn't possible to get data loaded from datastore at path " + path, e); + LOG.warn("It wasn't possible to get data loaded from datastore at path {}", path, e); } } @@ -270,7 +375,7 @@ public class BrokerFacade { final YangInstanceIdentifier path, final NormalizedNode payload, final SchemaContext schemaContext) { DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction(); - LOG.trace("Put " + datastore.name() + " via Restconf: {} with payload {}", path, payload); + LOG.trace("Put {} via Restconf: {} with payload {}", datastore.name(), path, payload); if (!ensureParentsByMerge(datastore, path, transaction, schemaContext)) { transaction.cancel(); transaction = domDataBroker.newReadWriteTransaction(); @@ -279,14 +384,29 @@ public class BrokerFacade { return transaction.submit(); } + private void putDataWithinTransaction( + final DOMDataReadWriteTransaction writeTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, final SchemaContext schemaContext) { + LOG.trace("Put {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload); + ensureParentsByMerge(datastore, path, writeTransaction, schemaContext); + writeTransaction.put(datastore, path, payload); + } + private CheckedFuture deleteDataViaTransaction( final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore, final YangInstanceIdentifier path) { - LOG.trace("Delete " + datastore.name() + " via Restconf: {}", path); + LOG.trace("Delete {} via Restconf: {}", datastore.name(), path); writeTransaction.delete(datastore, path); return writeTransaction.submit(); } + private void deleteDataWithinTransaction( + final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path) { + LOG.trace("Delete {} within Restconf PATCH: {}", datastore.name(), path); + writeTransaction.delete(datastore, path); + } + public void setDomDataBroker(final DOMDataBroker domDataBroker) { this.domDataBroker = domDataBroker; } diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCH.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCH.java new file mode 100644 index 0000000000..f2829712ae --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCH.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.rest.impl; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.ws.rs.HttpMethod; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@HttpMethod("PATCH") +@Documented +public @interface PATCH { +} \ No newline at end of file diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHContext.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHContext.java new file mode 100644 index 0000000000..adba67d7c5 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHContext.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.restconf.impl; + +import com.google.common.base.Preconditions; +import java.util.List; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; + +public class PATCHContext { + + private final InstanceIdentifierContext context; + private final List data; + private final String patchId; + + public PATCHContext(final InstanceIdentifierContext context, + final List data, final String patchId) { + this.context = Preconditions.checkNotNull(context); + this.data = Preconditions.checkNotNull(data); + this.patchId = Preconditions.checkNotNull(patchId); + } + + public InstanceIdentifierContext getInstanceIdentifierContext() { + return context; + } + + public List getData() { + return data; + } + + public String getPatchId() { + return patchId; + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEditOperation.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEditOperation.java new file mode 100644 index 0000000000..e0bdfd5893 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEditOperation.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.restconf.impl; + +/** + * + * Each YANG patch edit specifies one edit operation on the target data + * node. The set of operations is aligned with the NETCONF edit + * operations, but also includes some new operations. + * + */ +enum PATCHEditOperation { + CREATE, //post + DELETE, //delete + INSERT, //post + MERGE, + MOVE, //delete+post + REPLACE, //put + REMOVE //delete +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEntity.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEntity.java new file mode 100644 index 0000000000..ae16edc9d4 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEntity.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.restconf.impl; + +import com.google.common.base.Preconditions; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +public class PATCHEntity { + + private final String operation; + private final String editId; + private final YangInstanceIdentifier targetNode; + private final NormalizedNode node; + + public PATCHEntity(final String editId, final String operation, final YangInstanceIdentifier targetNode, final + NormalizedNode node) { + this.editId = Preconditions.checkNotNull(editId); + this.operation = Preconditions.checkNotNull(operation); + this.targetNode = Preconditions.checkNotNull(targetNode); + this.node = Preconditions.checkNotNull(node); + } + + public String getOperation() { + return operation; + } + + public String getEditId() { + return editId; + } + + public YangInstanceIdentifier getTargetNode() { + return targetNode; + } + + public NormalizedNode getNode() { + return node; + } + +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusContext.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusContext.java new file mode 100644 index 0000000000..f93dca10ff --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusContext.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.restconf.impl; + +import java.util.List; + +public class PATCHStatusContext { + + private final String patchId; + private final List editCollection; + private boolean ok; + private List globalErrors; + + public PATCHStatusContext(final String patchId, final List editCollection, + final boolean ok, final List globalErrors) { + this.patchId = patchId; + this.editCollection = editCollection; + this.ok = ok; + this.globalErrors = globalErrors; + } + + public String getPatchId() { + return patchId; + } + + public List getEditCollection() { + return editCollection; + } + + public boolean isOk() { + return ok; + } + + public List getGlobalErrors() { + return globalErrors; + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusEntity.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusEntity.java new file mode 100644 index 0000000000..f2329079b3 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusEntity.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.sal.restconf.impl; + +import java.util.List; + +public class PATCHStatusEntity { + + private final String editId; + private final List editErrors; + private final boolean ok; + + public PATCHStatusEntity(final String editId, final boolean ok, final List editErrors) { + this.editId = editId; + this.ok = ok; + this.editErrors = editErrors; + } + + public String getEditId() { + return editId; + } + + public boolean isOk() { + return ok; + } + + public List getEditErrors() { + return editErrors; + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java index fd1d190737..fedcd6bb6a 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2014, 2015 Brocade Communication Systems, Inc., Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the @@ -36,6 +36,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; @@ -978,6 +979,22 @@ public class RestconfImpl implements RestconfService { return Response.status(Status.OK).location(uriToWebsocketServer).build(); } + @Override + public PATCHStatusContext patchConfigurationData(String identifier, PATCHContext context, UriInfo uriInfo) { + if (context == null) { + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + return broker.patchConfigurationDataWithinTransaction(context, controllerContext.getGlobalSchema()); + } + + @Override + public PATCHStatusContext patchConfigurationData(PATCHContext context, @Context UriInfo uriInfo) { + if (context == null) { + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + return broker.patchConfigurationDataWithinTransaction(context, controllerContext.getGlobalSchema()); + } + /** * Load parameter for subscribing to stream from input composite node * diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java index 808a4afde6..78b0114f5c 100644 --- a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java +++ b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java @@ -211,6 +211,17 @@ public class StatisticsRestconfServiceWrapper implements RestconfService { return delegate.getAvailableStreams(uriInfo); } + @Override + public PATCHStatusContext patchConfigurationData(final String identifier, final PATCHContext payload, final UriInfo + uriInfo) { + return delegate.patchConfigurationData(identifier, payload, uriInfo); + } + + @Override + public PATCHStatusContext patchConfigurationData(final PATCHContext payload, final UriInfo uriInfo) { + return delegate.patchConfigurationData(payload, uriInfo); + } + public BigInteger getConfigDelete() { return BigInteger.valueOf(configDelete.get()); } diff --git a/opendaylight/restconf/sal-rest-connector/src/main/yang/instance-identifier-patch-module.yang b/opendaylight/restconf/sal-rest-connector/src/main/yang/instance-identifier-patch-module.yang new file mode 100644 index 0000000000..1eb39fe7cc --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/main/yang/instance-identifier-patch-module.yang @@ -0,0 +1,50 @@ +module instance-identifier-patch-module { + namespace "instance:identifier:patch:module"; + + prefix "iipmodule"; + revision 2015-11-21 { + } + + container patch-cont { + container patch-cont2 { + leaf cont-leaf { + type string; + } + } + + list my-list1 { + + description "PATCH /restconf/config/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + + key name; + + leaf name { + type string; + } + + leaf my-leaf11 { + type string; + } + + leaf my-leaf12 { + type string; + } + + list my-list2 { + key name; + + leaf name { + type string; + } + + leaf my-leaf21 { + type string; + } + + leaf my-leaf22 { + type string; + } + } + } + } +} \ No newline at end of file diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/md/sal/rest/common/TestRestconfUtils.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/md/sal/rest/common/TestRestconfUtils.java index 9dea0b95cd..029c3c9bf7 100644 --- a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/md/sal/rest/common/TestRestconfUtils.java +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/md/sal/rest/common/TestRestconfUtils.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java index f9a31955a5..6a728dc40c 100644 --- a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the @@ -26,6 +26,7 @@ import org.opendaylight.netconf.sal.rest.api.RestconfConstants; import org.opendaylight.netconf.sal.rest.impl.AbstractIdentifierAwareJaxRsProvider; import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; import org.opendaylight.yangtools.yang.model.api.SchemaContext; /** @@ -100,4 +101,11 @@ public abstract class AbstractBodyReaderTest { .getSchemaContext()); assertNotNull(nnContext.getInstanceIdentifierContext().getSchemaNode()); } + + protected static void checkPATCHContext(final PATCHContext patchContext) { + assertNotNull(patchContext.getData()); + assertNotNull(patchContext.getInstanceIdentifierContext().getInstanceIdentifier()); + assertNotNull(patchContext.getInstanceIdentifierContext().getSchemaContext()); + assertNotNull(patchContext.getInstanceIdentifierContext().getSchemaNode()); + } } diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReader.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReader.java new file mode 100644 index 0000000000..bcceb072c4 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReader.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.rest.impl.test.providers; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.io.InputStream; +import javax.ws.rs.core.MediaType; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.netconf.sal.rest.impl.JsonToPATCHBodyReader; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest { + + private final JsonToPATCHBodyReader jsonPATCHBodyReader; + private static SchemaContext schemaContext; + + public TestJsonPATCHBodyReader() throws NoSuchFieldException, SecurityException { + super(); + jsonPATCHBodyReader = new JsonToPATCHBodyReader(); + } + + @Override + protected MediaType getMediaType() { + return new MediaType(APPLICATION_JSON, null); + } + + @BeforeClass + public static void initialization() { + schemaContext = schemaContextLoader("/instanceidentifier/yang", schemaContext); + controllerContext.setSchemas(schemaContext); + } + + @Test + public void modulePATCHDataTest() throws Exception { + final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, jsonPATCHBodyReader, false); + + final InputStream inputStream = TestJsonBodyReader.class + .getResourceAsStream("/instanceidentifier/json/jsonPATCHdata.json"); + + final PATCHContext returnValue = jsonPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContext(returnValue); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java new file mode 100644 index 0000000000..9b98dd8caf --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.rest.impl.test.providers; + +import java.io.InputStream; +import javax.ws.rs.core.MediaType; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.netconf.sal.rest.impl.XmlToPATCHBodyReader; +import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +public class TestXmlPATCHBodyReader extends AbstractBodyReaderTest { + + private final XmlToPATCHBodyReader xmlPATCHBodyReader; + private static SchemaContext schemaContext; + + public TestXmlPATCHBodyReader() throws NoSuchFieldException, SecurityException { + super(); + xmlPATCHBodyReader = new XmlToPATCHBodyReader(); + } + + @Override + protected MediaType getMediaType() { + return new MediaType(MediaType.APPLICATION_XML, null); + } + + @BeforeClass + public static void initialization() throws NoSuchFieldException, SecurityException { + schemaContext = schemaContextLoader("/instanceidentifier/yang", schemaContext); + controllerContext.setSchemas(schemaContext); + } + + @Test + public void moduleDataTest() throws Exception { + final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + mockBodyReader(uri, xmlPATCHBodyReader, false); + final InputStream inputStream = TestXmlBodyReader.class + .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdata.xml"); + final PATCHContext returnValue = xmlPATCHBodyReader + .readFrom(null, null, null, mediaType, null, inputStream); + checkPATCHContext(returnValue); + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdata.json b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdata.json new file mode 100644 index 0000000000..cf1530ec4c --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdata.json @@ -0,0 +1,34 @@ +{ + "ietf-yang-patch:yang-patch" : { + + "patch-id" : "test-patch", + "comment" : "this is test patch", + "edit" : [ + { + "edit-id": "edit1", + "operation": "create", + "target": "/my-list2", + "value": { + "my-list2": { + "name": "my-leaf20", + "my-leaf21": "I am leaf21-0", + "my-leaf22": "I am leaf22-0" + } + } + }, + + { + "edit-id": "edit2", + "operation": "create", + "target": "/my-list2", + "value": { + "my-list2": { + "name": "my-leaf21", + "my-leaf21": "I am leaf21-1", + "my-leaf22": "I am leaf22-1" + } + } + } + ] + } +} diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdata.xml b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdata.xml new file mode 100644 index 0000000000..d7d3a6bea6 --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdata.xml @@ -0,0 +1,28 @@ + + test-patch + this is test patch + + edit1 + create + /my-list2 + + + my-leaf20 + I am leaf21-0 + I am leaf22-0 + + + + + edit2 + create + /my-list2 + + + my-leaf21 + I am leaf21-1 + I am leaf22-1 + + + + \ No newline at end of file diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/yang/instance-identifier-patch-module.yang b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/yang/instance-identifier-patch-module.yang new file mode 100644 index 0000000000..1eb39fe7cc --- /dev/null +++ b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/yang/instance-identifier-patch-module.yang @@ -0,0 +1,50 @@ +module instance-identifier-patch-module { + namespace "instance:identifier:patch:module"; + + prefix "iipmodule"; + revision 2015-11-21 { + } + + container patch-cont { + container patch-cont2 { + leaf cont-leaf { + type string; + } + } + + list my-list1 { + + description "PATCH /restconf/config/instance-identifier-patch-module:patch-cont/my-list1/leaf1"; + + key name; + + leaf name { + type string; + } + + leaf my-leaf11 { + type string; + } + + leaf my-leaf12 { + type string; + } + + list my-list2 { + key name; + + leaf name { + type string; + } + + leaf my-leaf21 { + type string; + } + + leaf my-leaf22 { + type string; + } + } + } + } +} \ No newline at end of file -- 2.36.6