Bug 3866: Support for Restconf HTTP Patch 63/36163/1
authorMartin Ciglan <mciglan@cisco.com>
Sun, 13 Mar 2016 17:29:21 +0000 (18:29 +0100)
committerMartin Ciglan <mciglan@cisco.com>
Sun, 13 Mar 2016 17:29:21 +0000 (18:29 +0100)
- 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 <mciglan@cisco.com>
24 files changed:
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/api/RestconfService.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPATCHBodyReader.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHJsonBodyWriter.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/PATCHXmlBodyWriter.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfApplication.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/RestconfCompositeWrapper.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPATCHBodyReader.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCH.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHContext.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEditOperation.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEntity.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusContext.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHStatusEntity.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java
opendaylight/restconf/sal-rest-connector/src/main/yang/instance-identifier-patch-module.yang [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/md/sal/rest/common/TestRestconfUtils.java
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/AbstractBodyReaderTest.java
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReader.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdata.json [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdata.xml [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/yang/instance-identifier-patch-module.yang [new file with mode: 0644]

index fac2b1872e2c9c9cf3d179b0717188731b5f9e48..178476fe7426c5f250296dbbf4b4fcfb3696cb01 100644 (file)
@@ -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 (file)
index 0000000..6d3f7ea
--- /dev/null
@@ -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<PATCHContext> {
+
+    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<PATCHContext> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> 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<PATCHEntity> resultList = read(jsonReader, path);
+        jsonReader.close();
+
+        return new PATCHContext(path, resultList, patchId);
+    }
+
+    private List<PATCHEntity> read(final JsonReader in, InstanceIdentifierContext path) throws
+            IOException {
+
+        boolean inEdit = false;
+        boolean inValue = false;
+        String operation = null;
+        String target = null;
+        String editId = null;
+        List<PATCHEntity> 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 (file)
index 0000000..9f9ab94
--- /dev/null
@@ -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<PATCHStatusContext> {
+
+    @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<String, Object> 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<RestconfError> 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 (file)
index 0000000..2d094a6
--- /dev/null
@@ -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<PATCHStatusContext> {
+
+    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<String, Object> 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<RestconfError> 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();
+    }
+}
index bfcd826f5d1ca9c295b1a11be4ae3c992472f4f3..46b8743832109605d6eb3399a3348a9c5f9108ad 100644 (file)
@@ -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)
index 4015d9ac22e59d04f49c26715e00fcc80a26a0bc..f184df0d05a8b6fb821839bc133c41549e87c3ae 100644 (file)
@@ -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 (file)
index 0000000..fe63f31
--- /dev/null
@@ -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<PATCHContext> {
+
+    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<PATCHContext> type, Type genericType, Annotation[] annotations, MediaType
+            mediaType, MultivaluedMap<String, String> 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<PATCHEntity> 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);
+    }
+}
index 0a5409cb10cde37af69f37fef0f08e450a5295a4..b4be7c683ccc119d6b75e302f09d2f7122abdcdb 100644 (file)
@@ -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<PATCHStatusEntity> editCollection = new ArrayList<>();
+        List<RestconfError> editErrors;
+        List<RestconfError> 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<Void, TransactionCommitFailedException> 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<Void, TransactionCommitFailedException> 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<Optional<NormalizedNode<?, ?>>> listenableFuture = transaction.read(datastore, path);
         if (listenableFuture != null) {
             Optional<NormalizedNode<?, ?>> 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<Boolean> 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<Void, TransactionCommitFailedException> 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 (file)
index 0000000..f282971
--- /dev/null
@@ -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 (file)
index 0000000..adba67d
--- /dev/null
@@ -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<? extends SchemaNode> context;
+    private final List<PATCHEntity> data;
+    private final String patchId;
+
+    public PATCHContext(final InstanceIdentifierContext<? extends SchemaNode> context,
+                        final List<PATCHEntity> data, final String patchId) {
+        this.context = Preconditions.checkNotNull(context);
+        this.data = Preconditions.checkNotNull(data);
+        this.patchId = Preconditions.checkNotNull(patchId);
+    }
+
+    public InstanceIdentifierContext<? extends SchemaNode> getInstanceIdentifierContext() {
+        return context;
+    }
+
+    public List<PATCHEntity> 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 (file)
index 0000000..e0bdfd5
--- /dev/null
@@ -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 (file)
index 0000000..ae16edc
--- /dev/null
@@ -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 (file)
index 0000000..f93dca1
--- /dev/null
@@ -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<PATCHStatusEntity> editCollection;
+    private boolean ok;
+    private List<RestconfError> globalErrors;
+
+    public PATCHStatusContext(final String patchId, final List<PATCHStatusEntity> editCollection,
+                              final boolean ok, final List<RestconfError> globalErrors) {
+        this.patchId = patchId;
+        this.editCollection = editCollection;
+        this.ok = ok;
+        this.globalErrors = globalErrors;
+    }
+
+    public String getPatchId() {
+        return patchId;
+    }
+
+    public List<PATCHStatusEntity> getEditCollection() {
+        return editCollection;
+    }
+
+    public boolean isOk() {
+        return ok;
+    }
+
+    public List<RestconfError> 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 (file)
index 0000000..f232907
--- /dev/null
@@ -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<RestconfError> editErrors;
+    private final boolean ok;
+
+    public PATCHStatusEntity(final String editId, final boolean ok, final List<RestconfError> editErrors) {
+        this.editId = editId;
+        this.ok = ok;
+        this.editErrors = editErrors;
+    }
+
+    public String getEditId() {
+        return editId;
+    }
+
+    public boolean isOk() {
+        return ok;
+    }
+
+    public List<RestconfError> getEditErrors() {
+        return editErrors;
+    }
+}
index fd1d1907370236ebb35b7ee69ea11c509a6014d6..fedcd6bb6ae4f7bae85e86a7a8c6474f13b42678 100644 (file)
@@ -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
      *
index 808a4afde6db7a9aac83b4973c01e639d6eb9301..78b0114f5c95336afaf3e902a1aa550df2df6dc5 100644 (file)
@@ -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 (file)
index 0000000..1eb39fe
--- /dev/null
@@ -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
index f9a31955a59986ec533ce69ab18fb13bb1a7205c..6a728dc40c3055ceac51328eb4d40a68a886f54f 100644 (file)
@@ -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 (file)
index 0000000..bcceb07
--- /dev/null
@@ -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 (file)
index 0000000..9b98dd8
--- /dev/null
@@ -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 (file)
index 0000000..cf1530e
--- /dev/null
@@ -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 (file)
index 0000000..d7d3a6b
--- /dev/null
@@ -0,0 +1,28 @@
+<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+    <patch-id>test-patch</patch-id>
+    <comment>this is test patch</comment>
+    <edit>
+        <edit-id>edit1</edit-id>
+        <operation>create</operation>
+        <target>/my-list2</target>
+        <value>
+            <my-list2 xmlns="instance:identifier:patch:module">
+                <name>my-leaf20</name>
+                <my-leaf21>I am leaf21-0</my-leaf21>
+                <my-leaf22>I am leaf22-0</my-leaf22>
+            </my-list2>
+        </value>
+    </edit>
+    <edit>
+        <edit-id>edit2</edit-id>
+        <operation>create</operation>
+        <target>/my-list2</target>
+        <value>
+            <my-list2 xmlns="instance:identifier:patch:module">
+                <name>my-leaf21</name>
+                <my-leaf21>I am leaf21-1</my-leaf21>
+                <my-leaf22>I am leaf22-1</my-leaf22>
+            </my-list2>
+        </value>
+    </edit>
+</yang-patch>
\ 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 (file)
index 0000000..1eb39fe
--- /dev/null
@@ -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