Allow null values for DocumentTree nodes

Change-Id: I88a12727751c6d82843a7b6a9a2e753da1500c99
diff --git a/core/api/src/main/java/org/onosproject/store/service/AsyncDocumentTree.java b/core/api/src/main/java/org/onosproject/store/service/AsyncDocumentTree.java
index 8b08b76..00bccda 100644
--- a/core/api/src/main/java/org/onosproject/store/service/AsyncDocumentTree.java
+++ b/core/api/src/main/java/org/onosproject/store/service/AsyncDocumentTree.java
@@ -49,8 +49,8 @@
      * Returns the value of the tree node at specified path.
      *
      * @param path path to the node
-     * @return future that will be either be completed with node value or {@code null} if path
-     * does not point to a valid node
+     * @return future that will be either be completed with node's {@link Versioned versioned} value
+     * or {@code null} if path does not point to a valid node
      */
     CompletableFuture<Versioned<V>> get(DocumentPath path);
 
@@ -58,9 +58,10 @@
      * Creates or updates a document tree node.
      *
      * @param path path to the node
-     * @param value the non-null value to be associated with the node
-     * @return future that will either be completed with the previous node value or {@code null} if there was no
-     * node previously at that path. Future will be completed with a {@code IllegalDocumentModificationException}
+     * @param value value to be associated with the node ({@code null} is a valid value)
+     * @return future that will either be completed with the previous {@link Versioned versioned}
+     * value or {@code null} if there was no node previously at that path.
+     * Future will be completed with a {@code IllegalDocumentModificationException}
      * if the parent node (for the node to create/update) does not exist
      */
     CompletableFuture<Versioned<V>> set(DocumentPath path, V value);
@@ -81,7 +82,7 @@
      * Creates a document tree node recursively by creating all missing intermediate nodes in the path.
      *
      * @param path path to the node
-     * @param value the non-null value to be associated with the key
+     * @param value value to be associated with the node ({@code null} is a valid value)
      * @return future that is completed with {@code true} if the new node was successfully
      * created. Future will be completed with {@code false} if a node already exists at the specified path
      */
@@ -91,10 +92,10 @@
      * Conditionally updates a tree node if the current version matches a specified version.
      *
      * @param path path to the node
-     * @param newValue the non-null value to be associated with the node
+     * @param newValue value to associate with the node ({@code null} is a valid value)
      * @param version current version of the node for update to occur
-     * @return future that is either completed with {@code true} if the update was made and the tree was
-     * modified or {@code false} if update did not happen
+     * @return future that is either completed with {@code true} if the update was made
+     * or {@code false} if update did not happen
      */
     CompletableFuture<Boolean> replace(DocumentPath path, V newValue, long version);
 
@@ -102,10 +103,10 @@
      * Conditionally updates a tree node if the current node value matches a specified version.
      *
      * @param path path to the node
-     * @param newValue the non-null value to be associated with the node
+     * @param newValue value to associate with the node ({@code null} is a valid value)
      * @param currentValue current value of the node for update to occur
-     * @return future that is either completed with {@code true} if the update was made and the tree was
-     * modified or {@code false} if update did not happen
+     * @return future that is either completed with {@code true} if the update was made
+     * or {@code false} if update did not happen
      */
     CompletableFuture<Boolean> replace(DocumentPath path, V newValue, V currentValue);
 
diff --git a/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/CatalystSerializers.java b/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/CatalystSerializers.java
index 3252f61..aea7aa7 100644
--- a/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/CatalystSerializers.java
+++ b/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/CatalystSerializers.java
@@ -21,6 +21,7 @@
 import io.atomix.variables.internal.LongCommands;
 
 import java.util.Arrays;
+import java.util.Optional;
 
 import org.onlab.util.Match;
 import org.onosproject.cluster.Leader;
@@ -110,6 +111,7 @@
         serializer.register(ImmutableList.of().getClass(), factory);
         serializer.register(ImmutableList.of("a").getClass(), factory);
         serializer.register(Arrays.asList().getClass(), factory);
+        serializer.register(Optional.class, factory);
 
         serializer.resolve(new LongCommands.TypeResolver());
         serializer.resolve(new AtomixConsistentMapCommands.TypeResolver());
diff --git a/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTree.java b/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTree.java
index 77f9b98..3875e55 100644
--- a/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTree.java
+++ b/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTree.java
@@ -27,6 +27,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
@@ -108,7 +109,7 @@
 
     @Override
     public CompletableFuture<Versioned<byte[]>> set(DocumentPath path, byte[] value) {
-        return client.submit(new Update(checkNotNull(path), checkNotNull(value), Match.any(), Match.any()))
+        return client.submit(new Update(checkNotNull(path), Optional.ofNullable(value), Match.any(), Match.any()))
                 .thenCompose(result -> {
                     if (result.status() == INVALID_PATH) {
                         return Tools.exceptionalFuture(new NoSuchDocumentPathException());
@@ -136,7 +137,7 @@
         return createInternal(path, value)
                 .thenCompose(status -> {
                     if (status == ILLEGAL_MODIFICATION) {
-                        return createRecursive(path.parent(), new byte[0])
+                        return createRecursive(path.parent(), null)
                                     .thenCompose(r -> createInternal(path, value).thenApply(v -> true));
                     }
                     return CompletableFuture.completedFuture(status == OK);
@@ -145,13 +146,19 @@
 
     @Override
     public CompletableFuture<Boolean> replace(DocumentPath path, byte[] newValue, long version) {
-        return client.submit(new Update(checkNotNull(path), newValue, Match.any(), Match.ifValue(version)))
+        return client.submit(new Update(checkNotNull(path),
+                                        Optional.ofNullable(newValue),
+                                        Match.any(),
+                                        Match.ifValue(version)))
                 .thenApply(result -> result.updated());
     }
 
     @Override
     public CompletableFuture<Boolean> replace(DocumentPath path, byte[] newValue, byte[] currentValue) {
-        return client.submit(new Update(checkNotNull(path), newValue, Match.ifValue(currentValue), Match.any()))
+        return client.submit(new Update(checkNotNull(path),
+                                        Optional.ofNullable(newValue),
+                                        Match.ifValue(currentValue),
+                                        Match.any()))
                 .thenCompose(result -> {
                     if (result.status() == INVALID_PATH) {
                         return Tools.exceptionalFuture(new NoSuchDocumentPathException());
@@ -168,7 +175,7 @@
         if (path.equals(DocumentPath.from("root"))) {
             return Tools.exceptionalFuture(new IllegalDocumentModificationException());
         }
-        return client.submit(new Update(checkNotNull(path), null, Match.ifNotNull(), Match.any()))
+        return client.submit(new Update(checkNotNull(path), null, Match.any(), Match.ifNotNull()))
                 .thenCompose(result -> {
                     if (result.status() == INVALID_PATH) {
                         return Tools.exceptionalFuture(new NoSuchDocumentPathException());
@@ -204,7 +211,7 @@
     }
 
     private CompletableFuture<DocumentTreeUpdateResult.Status> createInternal(DocumentPath path, byte[] value) {
-        return client.submit(new Update(checkNotNull(path), checkNotNull(value), Match.ifNull(), Match.any()))
+        return client.submit(new Update(checkNotNull(path), Optional.ofNullable(value), Match.any(), Match.ifNull()))
                      .thenApply(result -> result.status());
     }
 
diff --git a/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeCommands.java b/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeCommands.java
index f9f7bb1..9921b88 100644
--- a/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeCommands.java
+++ b/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeCommands.java
@@ -26,6 +26,7 @@
 import io.atomix.copycat.Query;
 
 import java.util.Map;
+import java.util.Optional;
 
 import org.onlab.util.Match;
 import org.onosproject.store.service.DocumentPath;
@@ -139,7 +140,7 @@
     @SuppressWarnings("serial")
     public static class Update extends DocumentTreeCommand<DocumentTreeUpdateResult<byte[]>> {
 
-        private byte[] value;
+        private Optional<byte[]> value;
         private Match<byte[]> valueMatch;
         private Match<Long> versionMatch;
 
@@ -150,14 +151,14 @@
             this.versionMatch = null;
         }
 
-        public Update(DocumentPath path, byte[] value, Match<byte[]> valueMatch, Match<Long> versionMatch) {
+        public Update(DocumentPath path, Optional<byte[]> value, Match<byte[]> valueMatch, Match<Long> versionMatch) {
             super(path);
             this.value = value;
             this.valueMatch = valueMatch;
             this.versionMatch = versionMatch;
         }
 
-        public byte[] value() {
+        public Optional<byte[]> value() {
             return value;
         }
 
diff --git a/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeState.java b/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeState.java
index 06d33b5..cfaa668 100644
--- a/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeState.java
+++ b/core/store/primitives/src/main/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeState.java
@@ -244,7 +244,7 @@
 
         @Override
         public byte[] value() {
-            return commit.operation().value();
+            return commit.operation().value().orElse(null);
         }
 
         @Override
diff --git a/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeTest.java b/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeTest.java
index eb52000..a26bde7 100644
--- a/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeTest.java
+++ b/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixDocumentTreeTest.java
@@ -89,6 +89,10 @@
 
         Versioned<byte[]> ac = tree.get(DocumentPath.from("root.a.c")).join();
         assertArrayEquals("ac".getBytes(), ac.value());
+
+        tree.create(DocumentPath.from("root.x"), null).join();
+        Versioned<byte[]> x = tree.get(DocumentPath.from("root.x")).join();
+        assertNull(x.value());
     }
 
     /**
@@ -100,10 +104,10 @@
                 AtomixDocumentTree.class).join();
         tree.createRecursive(DocumentPath.from("root.a.b.c"), "abc".getBytes()).join();
         Versioned<byte[]> a = tree.get(DocumentPath.from("root.a")).join();
-        assertArrayEquals(new byte[0], a.value());
+        assertArrayEquals(null, a.value());
 
         Versioned<byte[]> ab = tree.get(DocumentPath.from("root.a.b")).join();
-        assertArrayEquals(new byte[0], ab.value());
+        assertArrayEquals(null, ab.value());
 
         Versioned<byte[]> abc = tree.get(DocumentPath.from("root.a.b.c")).join();
         assertArrayEquals("abc".getBytes(), abc.value());
@@ -131,6 +135,10 @@
         tree.set(DocumentPath.from("root.a.b"), "newAB".getBytes()).join();
         Versioned<byte[]> newAB = tree.get(DocumentPath.from("root.a.b")).join();
         assertArrayEquals("newAB".getBytes(), newAB.value());
+
+        tree.set(DocumentPath.from("root.x"), null).join();
+        Versioned<byte[]> x = tree.get(DocumentPath.from("root.x")).join();
+        assertNull(x.value());
     }
 
     /**
@@ -199,6 +207,11 @@
         Versioned<byte[]> a = tree.removeNode(DocumentPath.from("root.a")).join();
         assertArrayEquals("a".getBytes(), a.value());
         assertNull(tree.get(DocumentPath.from("root.a")).join());
+
+        tree.create(DocumentPath.from("root.x"), null).join();
+        Versioned<byte[]> x = tree.removeNode(DocumentPath.from("root.x")).join();
+        assertNull(x.value());
+        assertNull(tree.get(DocumentPath.from("root.a.x")).join());
     }
 
     /**