HONEYCOMB-431: make DataModification.validate idempotent 17/14017/2
authorMarek Gradzki <mgradzki@cisco.com>
Fri, 13 Apr 2018 11:38:16 +0000 (13:38 +0200)
committerMarek Gradzki <mgradzki@cisco.com>
Fri, 17 Aug 2018 10:16:40 +0000 (10:16 +0000)
This patch modifies contract of DataModification.validate
to make it idempotent.

ModifiableDataTreeManager.validate now invokes dataTree.validate
on a copy of DataTreeModification.

ModifiableDataTreeManager.validateCandidate was introduced
to allow additional validation.

Change-Id: I86fc101faff9b04afde2f3eb16fff4d4df2867ad
Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
infra/data-api/src/main/java/io/fd/honeycomb/data/DataModification.java
infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModifiableDataTreeManager.java
infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegatorTest.java
infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/WriteTransactionTest.java
infra/translate-api/src/main/java/io/fd/honeycomb/translate/ValidationFailedException.java [new file with mode: 0644]

index 22fba0f..a87983c 100644 (file)
@@ -18,9 +18,9 @@ package io.fd.honeycomb.data;
 
 import com.google.common.annotations.Beta;
 import io.fd.honeycomb.translate.TranslationException;
+import io.fd.honeycomb.translate.ValidationFailedException;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 
 /**
  * Modification of a {@link ModifiableDataManager}.
@@ -55,18 +55,21 @@ public interface DataModification extends ReadableDataManager, AutoCloseable {
     /**
      * Alters data tree using this modification.
      *
-     * @throws DataValidationFailedException if modification data is not valid
-     * @throws TranslationException if failed while updating data tree state
+     * @throws TranslationException if commit failed while updating data tree state
      */
-    void commit() throws DataValidationFailedException, TranslationException;
+    void commit() throws TranslationException;
 
     /**
-     * Validate and prepare modification before commit. Besides commit, no further operation is expected after validate
-     * and the behaviour is undefined.
+     * Validates state of the {@link DataModification}.
      *
-     * @throws DataValidationFailedException if modification data is not valid
+     * <p>The operation does not have any side-effects on the modification state.
+     *
+     * <p>It can be executed many times, providing the same results
+     * if the state of the modification has not been changed.
+     *
+     * @throws ValidationFailedException if modification data is not valid
      */
-    void validate() throws DataValidationFailedException;
+    void validate() throws ValidationFailedException;
 
     /**
      * Perform cleanup if necessary.
@@ -75,4 +78,5 @@ public interface DataModification extends ReadableDataManager, AutoCloseable {
     default void close() {
         // by default, no cleanup is required
     }
+
 }
index f16172f..aa5b3e5 100644 (file)
 package io.fd.honeycomb.data.impl;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.util.concurrent.Futures.immediateCheckedFuture;
 
 import com.google.common.base.Optional;
 import com.google.common.util.concurrent.CheckedFuture;
 import io.fd.honeycomb.data.DataModification;
 import io.fd.honeycomb.data.ModifiableDataManager;
+import io.fd.honeycomb.translate.ValidationFailedException;
 import io.fd.honeycomb.translate.TranslationException;
 import javax.annotation.Nonnull;
 import org.apache.commons.lang3.builder.RecursiveToStringStyle;
@@ -30,9 +32,12 @@ import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.CursorAwareDataTreeModification;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeModification;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeModificationCursor;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,11 +66,12 @@ public class ModifiableDataTreeManager implements ModifiableDataManager {
     }
 
     protected class ConfigSnapshot implements DataModification {
+        private final DataTreeSnapshot snapshot;
         private final DataTreeModification modification;
-        private boolean validated = false;
 
         ConfigSnapshot() {
-            this.modification = dataTree.takeSnapshot().newModification();
+            this.snapshot = dataTree.takeSnapshot();
+            this.modification = snapshot.newModification();
         }
 
         @Override
@@ -95,39 +101,58 @@ public class ModifiableDataTreeManager implements ModifiableDataManager {
         }
 
         @Override
-        public final void commit() throws DataValidationFailedException, TranslationException {
-            if(!validated) {
-                validate();
-            }
-            final DataTreeCandidate candidate = dataTree.prepare(modification);
+        public final void commit() throws TranslationException {
+            final DataTreeCandidate candidate = prepareCandidate(modification);
+            validateCandidate(candidate);
             processCandidate(candidate);
             dataTree.commit(candidate);
         }
 
+        private DataTreeCandidate prepareCandidate(final DataTreeModification dataTreeModification)
+            throws ValidationFailedException {
+            // Seal the modification (required to perform validate)
+            dataTreeModification.ready();
+
+            // Check if modification can be applied to data tree
+            try {
+                dataTree.validate(dataTreeModification);
+            } catch (DataValidationFailedException e) {
+                throw new ValidationFailedException(e);
+            }
+
+            return dataTree.prepare(dataTreeModification);
+        }
+
+        protected void validateCandidate(final DataTreeCandidate candidate) throws ValidationFailedException {
+            // NOOP
+        }
+
         protected void processCandidate(final DataTreeCandidate candidate) throws TranslationException {
             // NOOP
         }
 
         @Override
-        public final void validate() throws DataValidationFailedException {
-            modification.ready();
-            dataTree.validate(modification);
-            validated = true;
+        public final void validate() throws ValidationFailedException {
+            // Modification requires to be sealed before validation.
+            // Sealed modification cannot be altered, so create copy.
+            final CursorAwareDataTreeModification modificationCopy =
+                (CursorAwareDataTreeModification) snapshot.newModification();
+            final DataTreeModificationCursor cursor = modificationCopy.createCursor(dataTree.getRootPath());
+            checkState(cursor != null, "DataTreeModificationCursor for root path should not be null");
+            modification.applyToCursor(cursor);
+            // Then validate it.
+            validateCandidate(prepareCandidate(modificationCopy));
         }
 
         @Override
         public String toString() {
-            return "ConfigSnapshot{" +
-                    "modification=" +
-                    ReflectionToStringBuilder.toString(
-                            modification,
-                            RecursiveToStringStyle.MULTI_LINE_STYLE,
-                            false,
-                            false
-                    ) + ", validated=" + validated + '}';
+            return "ConfigSnapshot{modification="
+                + ReflectionToStringBuilder.toString(
+                modification,
+                RecursiveToStringStyle.MULTI_LINE_STYLE,
+                false,
+                false
+            ) + '}';
         }
     }
 }
-
-
-
index 3065a94..ef9d3d6 100644 (file)
@@ -67,6 +67,16 @@ public class ModifiableDataTreeDelegatorTest extends ModifiableDataTreeDelegator
         assertEquals(dataTree.takeSnapshot().readNode(TOP_CONTAINER_ID), Optional.toJavaUtil(normalizedNodeOptional));
     }
 
+    @Test
+    public void testValidateTwice() throws Exception {
+        final MapNode nestedList = getNestedList("listEntry", "listValue");
+
+        final DataModification dataModification = configDataTree.newModification();
+        dataModification.write(NESTED_LIST_ID, nestedList);
+        dataModification.validate();
+        dataModification.validate();
+    }
+
     @Test
     public void testCommitSuccessful() throws Exception {
         final MapNode nestedList = getNestedList("listEntry", "listValue");
index 1dab552..89ecef2 100644 (file)
@@ -27,6 +27,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
 
 import com.google.common.util.concurrent.CheckedFuture;
 import io.fd.honeycomb.data.DataModification;
+import io.fd.honeycomb.translate.ValidationFailedException;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -34,7 +35,6 @@ import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 
 public class WriteTransactionTest {
 
@@ -104,7 +104,7 @@ public class WriteTransactionTest {
 
     @Test
     public void testSubmitFailed() throws Exception {
-        doThrow(mock(DataValidationFailedException.class)).when(configSnapshot).commit();
+        doThrow(mock(ValidationFailedException.class)).when(configSnapshot).commit();
         final CheckedFuture<Void, TransactionCommitFailedException> future = writeTx.submit();
         try {
             future.get();
diff --git a/infra/translate-api/src/main/java/io/fd/honeycomb/translate/ValidationFailedException.java b/infra/translate-api/src/main/java/io/fd/honeycomb/translate/ValidationFailedException.java
new file mode 100644 (file)
index 0000000..7767162
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.honeycomb.translate;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Thrown when validation fails at the translation layer.
+ */
+public class ValidationFailedException extends TranslationException {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs an {@link ValidationFailedException} given exception detail message.
+     *
+     * @param message the exception detail message
+     */
+    public ValidationFailedException(@Nonnull final String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs an {@link ValidationFailedException} given exception detail message and exception cause.
+     *
+     * @param cause   the cause of validation failure
+     * @param message the exception detail message
+     */
+    public ValidationFailedException(@Nonnull final String message, @Nonnull final Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs an {@link ValidationFailedException} given exception cause.
+     *
+     * @param cause the cause of validated failure
+     */
+    public ValidationFailedException(@Nonnull final Throwable cause) {
+        super(cause);
+    }
+}