HONEYCOMB-431: add validation support to Writers 22/14022/4
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:17:58 +0000 (10:17 +0000)
This patch introduces FlatWriterRegistry.validateModifications.
Implementation iterates over writersOrder following bulkUpdate logic
to properly support subtree writers case.

Writers are now cabable of validating modifications.
Commonly used implementations (GenericWriter and GenericListWriter)
delegate validation capbility to Validators.

Change-Id: If7a0bb0838c0b8f2c0393c989f3b03853a2ea679
Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
12 files changed:
infra/translate-api/src/main/java/io/fd/honeycomb/translate/write/DataValidationFailedException.java
infra/translate-api/src/main/java/io/fd/honeycomb/translate/write/Validator.java [new file with mode: 0644]
infra/translate-api/src/main/java/io/fd/honeycomb/translate/write/Writer.java
infra/translate-impl/src/main/java/io/fd/honeycomb/translate/impl/write/GenericListWriter.java
infra/translate-impl/src/main/java/io/fd/honeycomb/translate/impl/write/GenericWriter.java
infra/translate-impl/src/main/java/io/fd/honeycomb/translate/impl/write/registry/FlatWriterRegistry.java
infra/translate-impl/src/main/java/io/fd/honeycomb/translate/impl/write/registry/SubtreeWriter.java
infra/translate-impl/src/test/java/io/fd/honeycomb/translate/impl/write/GenericListWriterTest.java
infra/translate-impl/src/test/java/io/fd/honeycomb/translate/impl/write/GenericWriterTest.java
infra/translate-impl/src/test/java/io/fd/honeycomb/translate/impl/write/registry/FlatWriterRegistryTest.java
infra/translate-spi/src/main/java/io/fd/honeycomb/translate/spi/write/WriterCustomizer.java
infra/translate-utils/src/main/java/io/fd/honeycomb/translate/util/write/AbstractGenericWriter.java

index 3c0392e..d728bee 100644 (file)
@@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 
 import io.fd.honeycomb.translate.ValidationFailedException;
 import javax.annotation.Nonnull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 
 /**
@@ -76,4 +77,77 @@ public class DataValidationFailedException extends ValidationFailedException {
     public InstanceIdentifier<?> getFailedId() {
         return failedId;
     }
+
+    /**
+     * Create specific validated failed exception.
+     */
+    public static class CreateValidationFailedException extends DataValidationFailedException {
+        private static final long serialVersionUID = 1;
+
+        private final transient DataObject data;
+
+        public CreateValidationFailedException(@Nonnull final InstanceIdentifier<?> failedId,
+                                               @Nonnull final DataObject data,
+                                               @Nonnull final Throwable cause) {
+            super(failedId, getMsg(failedId, data), cause);
+            this.data = checkNotNull(data, "data");
+        }
+
+        private static String getMsg(final @Nonnull InstanceIdentifier<?> failedId,
+                                     final @Nonnull DataObject data) {
+            return String.format("Failed to validate create request for: %s at: %s.", data, failedId);
+        }
+
+        public DataObject getData() {
+            return data;
+        }
+    }
+
+    /**
+     * Update specific validated failed exception.
+     */
+    public static class UpdateValidationFailedException extends DataValidationFailedException {
+        private static final long serialVersionUID = 1;
+
+        private final transient DataObject dataBefore;
+        private final transient DataObject dataAfter;
+
+        public UpdateValidationFailedException(@Nonnull final InstanceIdentifier<?> failedId,
+                                               @Nonnull final DataObject dataBefore,
+                                               @Nonnull final DataObject dataAfter,
+                                               @Nonnull final Throwable cause) {
+            super(failedId, getMsg(failedId, dataBefore, dataAfter), cause);
+            this.dataBefore = checkNotNull(dataBefore, "dataBefore");
+            this.dataAfter = checkNotNull(dataAfter, "dataAfter");
+        }
+
+        private static String getMsg(final @Nonnull InstanceIdentifier<?> failedId, final DataObject dataBefore,
+                                     final @Nonnull DataObject dataAfter) {
+            return String
+                .format("Failed to validate update request from: %s to: %s, at: %s.", dataBefore, dataAfter, failedId);
+        }
+
+        public DataObject getDataBefore() {
+            return dataBefore;
+        }
+
+        public DataObject getDataAfter() {
+            return dataAfter;
+        }
+    }
+
+    /**
+     * Delete specific validated failed exception.
+     */
+    public static class DeleteValidationFailedException extends DataValidationFailedException {
+        private static final long serialVersionUID = 1;
+
+        public DeleteValidationFailedException(@Nonnull final InstanceIdentifier<?> failedId, @Nonnull final Throwable cause) {
+            super(failedId, getMsg(failedId), cause);
+        }
+
+        private static String getMsg(@Nonnull final InstanceIdentifier<?> failedId) {
+            return String.format("Failed to validate delete request: %s.", failedId);
+        }
+    }
 }
diff --git a/infra/translate-api/src/main/java/io/fd/honeycomb/translate/write/Validator.java b/infra/translate-api/src/main/java/io/fd/honeycomb/translate/write/Validator.java
new file mode 100644 (file)
index 0000000..2044757
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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.write;
+
+import com.google.common.annotations.Beta;
+import io.fd.honeycomb.translate.write.DataValidationFailedException.CreateValidationFailedException;
+import io.fd.honeycomb.translate.write.DataValidationFailedException.DeleteValidationFailedException;
+import io.fd.honeycomb.translate.write.DataValidationFailedException.UpdateValidationFailedException;
+import javax.annotation.Nonnull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Responsible for validation of DataObjects.
+ * Handles all update operations (create, update, delete).
+ *
+ * @param <D> Specific DataObject derived type, that is handled by this writer
+ * @see Writer#validate(InstanceIdentifier, DataObject, DataObject, WriteContext)
+ */
+@Beta
+public interface Validator<D extends DataObject> {
+    /**
+     * Validates write operation.
+     *
+     * @param id           Identifier(from root) of data being written
+     * @param dataAfter    New data to be written
+     * @param writeContext Write context that provides information about current state of DataTree.
+     * @throws CreateValidationFailedException if write validation failed
+     */
+    void validateWrite(
+        @Nonnull final InstanceIdentifier<D> id,
+        @Nonnull final D dataAfter,
+        @Nonnull final WriteContext writeContext) throws CreateValidationFailedException;
+
+    /**
+     * Validates update operation.
+     *
+     * @param id           Identifier(from root) of data being updated
+     * @param dataBefore   Old data
+     * @param dataAfter    New, updated data
+     * @param writeContext Write context that provides information about current state of DataTree.
+     * @throws UpdateValidationFailedException if update validation failed
+     */
+    void validateUpdate(InstanceIdentifier<D> id, D dataBefore, D dataAfter, WriteContext writeContext)
+        throws UpdateValidationFailedException;
+
+    /**
+     * Validates delete operation.
+     *
+     * @param id           Identifier(from root) of data being written
+     * @param dataBefore   Old data being deleted
+     * @param writeContext Write context that provides information about current state of DataTree.
+     * @throws DeleteValidationFailedException if delete validation failed
+     */
+    void validateDelete(InstanceIdentifier<D> id, D dataBefore, WriteContext writeContext)
+        throws DeleteValidationFailedException;
+}
index 18573e5..f3265a3 100644 (file)
@@ -32,6 +32,20 @@ import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 @Beta
 public interface Writer<D extends DataObject> extends SubtreeManager<D> {
 
+    /**
+     * Validates data modification.
+     *
+     * @param id Identifier of data being validated
+     * @param dataBefore Old data
+     * @param dataAfter New, updated data
+     * @param ctx Write context enabling writer to get information about candidate data as well as current data
+     */
+    default void validate(@Nonnull final InstanceIdentifier<? extends DataObject> id,
+                          @Nullable final DataObject dataBefore,
+                          @Nullable final DataObject dataAfter,
+                          @Nonnull final WriteContext ctx) throws DataValidationFailedException {
+    }
+
     /**
      * Process modifications and translate them as create/update/delete operations to lower level
      *
index 92467a8..5121eb7 100644 (file)
@@ -19,12 +19,13 @@ package io.fd.honeycomb.translate.impl.write;
 import static io.fd.honeycomb.translate.impl.write.GenericWriter.isUpdateSupported;
 
 import io.fd.honeycomb.translate.spi.write.ListWriterCustomizer;
+import io.fd.honeycomb.translate.spi.write.WriterCustomizer;
 import io.fd.honeycomb.translate.util.RWUtils;
 import io.fd.honeycomb.translate.util.write.AbstractGenericWriter;
 import io.fd.honeycomb.translate.write.ListWriter;
+import io.fd.honeycomb.translate.write.Validator;
 import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.WriteFailedException;
-import io.fd.honeycomb.translate.spi.write.WriterCustomizer;
 import javax.annotation.Nonnull;
 import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.binding.Identifiable;
@@ -45,6 +46,13 @@ public final class GenericListWriter<D extends DataObject & Identifiable<K>, K e
         this.customizer = customizer;
     }
 
+    public GenericListWriter(@Nonnull final InstanceIdentifier<D> type,
+                             @Nonnull final ListWriterCustomizer<D, K> customizer,
+                             @Nonnull final Validator<D> validator) {
+        super(type, isUpdateSupported(customizer), validator);
+        this.customizer = customizer;
+    }
+
     @Override
     protected void writeCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, @Nonnull final D data,
                                           @Nonnull final WriteContext ctx) throws WriteFailedException {
@@ -79,22 +87,25 @@ public final class GenericListWriter<D extends DataObject & Identifiable<K>, K e
     @Override
     protected void writeCurrent(final InstanceIdentifier<D> id, final D data, final WriteContext ctx)
         throws WriteFailedException {
-        super.writeCurrent(getId(id, data), data, ctx);
+        super.writeCurrent(getManagedId(id, data), data, ctx);
     }
 
     @Override
     protected void updateCurrent(final InstanceIdentifier<D> id, final D dataBefore, final D dataAfter,
                                  final WriteContext ctx) throws WriteFailedException {
-        super.updateCurrent(getId(id, dataBefore), dataBefore, dataAfter, ctx);
+        super.updateCurrent(getManagedId(id, dataBefore), dataBefore, dataAfter, ctx);
     }
 
     @Override
     protected void deleteCurrent(final InstanceIdentifier<D> id, final D dataBefore, final WriteContext ctx)
         throws WriteFailedException {
-        super.deleteCurrent(getId(id, dataBefore), dataBefore, ctx);
+        super.deleteCurrent(getManagedId(id, dataBefore), dataBefore, ctx);
     }
 
-    private InstanceIdentifier<D> getId(final InstanceIdentifier<D> id, final D current) {
+    @Override
+    protected InstanceIdentifier<D> getManagedId(@Nonnull final InstanceIdentifier<? extends DataObject> currentId,
+                                                 @Nonnull final D current) {
+        final InstanceIdentifier<D> id = (InstanceIdentifier<D>) currentId;
         // Make sure the key is present
         if (isWildcarded(id)) {
             return getSpecificId(id, current);
index 086936e..bfdf072 100644 (file)
 
 package io.fd.honeycomb.translate.impl.write;
 
-import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.spi.write.WriterCustomizer;
 import io.fd.honeycomb.translate.util.write.AbstractGenericWriter;
+import io.fd.honeycomb.translate.write.Validator;
+import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.WriteFailedException;
 import javax.annotation.Nonnull;
 import org.opendaylight.yangtools.yang.binding.DataObject;
@@ -39,7 +40,13 @@ public final class GenericWriter<D extends DataObject> extends AbstractGenericWr
                          @Nonnull final WriterCustomizer<D> customizer) {
         super(type, isUpdateSupported(customizer));
         this.customizer = customizer;
+    }
 
+    public GenericWriter(@Nonnull final InstanceIdentifier<D> type,
+                         @Nonnull final WriterCustomizer<D> customizer,
+                         @Nonnull final Validator<D> validator) {
+        super(type, isUpdateSupported(customizer), validator);
+        this.customizer = customizer;
     }
 
     static boolean isUpdateSupported(final @Nonnull WriterCustomizer<?> customizer) {
index 67a5da3..9e36e59 100644 (file)
@@ -28,12 +28,14 @@ import com.google.common.collect.Sets;
 import io.fd.honeycomb.translate.TranslationException;
 import io.fd.honeycomb.translate.util.RWUtils;
 import io.fd.honeycomb.translate.write.DataObjectUpdate;
+import io.fd.honeycomb.translate.write.DataValidationFailedException;
 import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.Writer;
 import io.fd.honeycomb.translate.write.registry.UpdateFailedException;
 import io.fd.honeycomb.translate.write.registry.WriterRegistry;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -75,6 +77,33 @@ final class FlatWriterRegistry implements WriterRegistry {
         this.writers = writersById.entrySet().stream().map(Map.Entry::getValue).collect(Collectors.toSet());
     }
 
+    @Override
+    public void validateModifications(@Nonnull final DataObjectUpdates updates, @Nonnull final WriteContext ctx)
+        throws DataValidationFailedException {
+        // Optimization: validation order is not relevant, so do not merge deletes and updates.
+        validateModifications(updates.getDeletes(), ctx);
+        validateModifications(updates.getUpdates(), ctx);
+    }
+
+    private void validateModifications(
+        @Nonnull final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates,
+        @Nonnull final WriteContext ctx) throws DataValidationFailedException {
+        if (updates.isEmpty()) {
+            return;
+        }
+        // Fail early if some handlers are missing.
+        checkAllTypesCanBeHandled(updates);
+
+        // Validators do not modify anything, so order of validators is not important.
+        // We iterate over writersOrder instead of modifications for consistent handling of subtree writers case.
+        for (InstanceIdentifier<?> writerType : writersOrder) {
+            final Writer<?> writer = getWriter(writerType);
+            for (DataObjectUpdate singleUpdate : getWritersData(updates, writer, writerType, ctx)) {
+                writer.validate(singleUpdate.getId(), singleUpdate.getDataBefore(), singleUpdate.getDataAfter(), ctx);
+            }
+        }
+    }
+
     @Override
     public void processModifications(@Nonnull final DataObjectUpdates updates,
                                      @Nonnull final WriteContext ctx) throws TranslationException {
@@ -207,26 +236,11 @@ final class FlatWriterRegistry implements WriterRegistry {
         LOG.debug("Performing bulk update for: {}", updates.keySet());
         // Iterate over all writers and call update if there are any related updates
         for (InstanceIdentifier<?> writerType : writersOrder) {
-            Collection<? extends DataObjectUpdate> writersData = updates.get(writerType);
             final Writer<?> writer = getWriter(writerType);
-
-            if (writersData.isEmpty()) {
-                // If there are no data for current writer, but it is a SubtreeWriter and there are updates to
-                // its children, still invoke it with its root data
-                if (writer instanceof SubtreeWriter<?> && isAffected((SubtreeWriter<?>) writer, updates)) {
-                    // Provide parent data for SubtreeWriter for further processing
-                    writersData = getParentDataObjectUpdate(ctx, updates, writer);
-                } else {
-                    // Skipping unaffected writer
-                    // Alternative to this would be modification sort according to the order of writers
-                    continue;
-                }
-            }
-
             LOG.debug("Performing update for: {}", writerType);
             LOG.trace("Performing update with writer: {}", writer);
 
-            for (DataObjectUpdate singleUpdate : writersData) {
+            for (DataObjectUpdate singleUpdate : getWritersData(updates, writer, writerType, ctx)) {
                 try {
                     writer.processModification(singleUpdate.getId(), singleUpdate.getDataBefore(),
                             singleUpdate.getDataAfter(), ctx);
@@ -240,6 +254,29 @@ final class FlatWriterRegistry implements WriterRegistry {
         }
     }
 
+    private Collection<? extends DataObjectUpdate> getWritersData(
+        final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates, final Writer<?> writer,
+        final InstanceIdentifier<?> writerType, final WriteContext ctx) {
+        Collection<? extends DataObjectUpdate> writersData = updates.get(writerType);
+
+        if (writersData.isEmpty()) {
+            // If there are no data for current writer, but it is a SubtreeWriter and there are updates to
+            // its children, still invoke it with its root data.
+            // Notice that child updates will be ignored if the set of all modifications
+            // contain both parent update and child updates. But this is ok, since all changes are already expressed in
+            // the parent update.
+            if (writer instanceof SubtreeWriter<?> && isAffected((SubtreeWriter<?>) writer, updates)) {
+                // Provide parent data for SubtreeWriter for further processing
+                writersData = getParentDataObjectUpdate(ctx, updates, writer);
+            } else {
+                // Skipping unaffected writer
+                // Alternative to this would be modification sort according to the order of writers
+                return Collections.emptyList();
+            }
+        }
+        return writersData;
+    }
+
     private void checkAllTypesCanBeHandled(
             @Nonnull final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates) {
 
index a1a5f3f..e510bc3 100644 (file)
@@ -19,6 +19,7 @@ package io.fd.honeycomb.translate.impl.write.registry;
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.collect.Iterables;
+import io.fd.honeycomb.translate.write.DataValidationFailedException;
 import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.WriteFailedException;
 import io.fd.honeycomb.translate.write.Writer;
@@ -67,6 +68,13 @@ final class SubtreeWriter<D extends DataObject> implements Writer<D> {
         return handledChildTypes;
     }
 
+    @Override
+    public void validate(@Nonnull final InstanceIdentifier<? extends DataObject> id,
+                         @Nullable final DataObject dataBefore, @Nullable final DataObject dataAfter,
+                         @Nonnull final WriteContext ctx) throws DataValidationFailedException {
+        delegate.validate(id, dataBefore, dataAfter, ctx);
+    }
+
     @Override
     public void processModification(
             @Nonnull final InstanceIdentifier<? extends DataObject> id,
index 91785b2..20cabc5 100644 (file)
@@ -22,6 +22,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import io.fd.honeycomb.translate.spi.write.ListWriterCustomizer;
+import io.fd.honeycomb.translate.write.Validator;
 import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.WriteFailedException;
 import java.util.Collections;
@@ -42,7 +43,6 @@ public class GenericListWriterTest {
     private ListWriterCustomizer<IdentifiableDataObject, DataObjectIdentifier> customizer;
     @Mock
     private WriteContext ctx;
-    private GenericListWriter<IdentifiableDataObject, DataObjectIdentifier> writer;
     @Mock
     private IdentifiableDataObject before;
     @Mock
@@ -51,11 +51,15 @@ public class GenericListWriterTest {
     private IdentifiableDataObject after;
     @Mock
     private DataObjectIdentifier keyAfter;
+    @Mock
+    private Validator<IdentifiableDataObject> validator;
+
+    private GenericListWriter<IdentifiableDataObject, DataObjectIdentifier> writer;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        writer = new GenericListWriter<>(DATA_OBJECT_ID, customizer);
+        writer = new GenericListWriter<>(DATA_OBJECT_ID, customizer, validator);
         when(before.getKey()).thenReturn(beforeKey);
         when(after.getKey()).thenReturn(keyAfter);
     }
@@ -106,4 +110,25 @@ public class GenericListWriterTest {
         writer = new GenericListWriter<>(DATA_OBJECT_ID, customizer);
         writer.deleteCurrentAttributes(DATA_OBJECT_ID, before, ctx);
     }
+
+    @Test
+    public void testValidate() throws Exception {
+        assertEquals(DATA_OBJECT_ID, writer.getManagedDataObjectType());
+
+        final InstanceIdentifier<IdentifiableDataObject> keyedIdBefore =
+            (InstanceIdentifier<IdentifiableDataObject>) InstanceIdentifier.create(Collections
+                .singleton(new InstanceIdentifier.IdentifiableItem<>(IdentifiableDataObject.class, beforeKey)));
+        final InstanceIdentifier<IdentifiableDataObject> keyedIdAfter =
+            (InstanceIdentifier<IdentifiableDataObject>) InstanceIdentifier.create(Collections
+                .singleton(new InstanceIdentifier.IdentifiableItem<>(IdentifiableDataObject.class, keyAfter)));
+
+        writer.validate(DATA_OBJECT_ID, before, after, ctx);
+        verify(validator).validateUpdate(keyedIdBefore, before, after, ctx);
+
+        writer.validate(DATA_OBJECT_ID, before, null, ctx);
+        verify(validator).validateDelete(keyedIdBefore, before, ctx);
+
+        writer.validate(DATA_OBJECT_ID, null, after, ctx);
+        verify(validator).validateWrite(keyedIdAfter, after, ctx);
+    }
 }
\ No newline at end of file
index c9d381a..1c0927b 100644 (file)
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.verify;
 
 import io.fd.honeycomb.translate.spi.write.WriterCustomizer;
+import io.fd.honeycomb.translate.write.Validator;
 import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.WriteFailedException;
 import org.junit.Before;
@@ -40,16 +41,19 @@ public class GenericWriterTest {
     private WriterCustomizer<DataObject> customizer;
     @Mock
     private WriteContext ctx;
-    private GenericWriter<DataObject> writer;
     @Mock
     private DataObject before;
     @Mock
     private DataObject after;
+    @Mock
+    private Validator<DataObject> validator;
+
+    private GenericWriter<DataObject> writer;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        writer = new GenericWriter<>(DATA_OBJECT_ID, customizer);
+        writer = new GenericWriter<>(DATA_OBJECT_ID, customizer, validator);
     }
 
     @Test
@@ -94,4 +98,17 @@ public class GenericWriterTest {
         assertTrue(GenericWriter.isUpdateSupported(new NoopWriters.DirectUpdateWriterCustomizer()));
         assertTrue(GenericWriter.isUpdateSupported(new NoopWriters.ParentImplDirectUpdateWriterCustomizer()));
     }
+
+    @Test
+    public void testValidate() throws Exception {
+        assertEquals(DATA_OBJECT_ID, writer.getManagedDataObjectType());
+        writer.validate(DATA_OBJECT_ID, before, after, ctx);
+        verify(validator).validateUpdate(DATA_OBJECT_ID, before, after, ctx);
+
+        writer.validate(DATA_OBJECT_ID, before, null, ctx);
+        verify(validator).validateDelete(DATA_OBJECT_ID, before, ctx);
+
+        writer.validate(DATA_OBJECT_ID, null, after, ctx);
+        verify(validator).validateWrite(DATA_OBJECT_ID, after, ctx);
+    }
 }
\ No newline at end of file
index 7f4b93e..e06197f 100644 (file)
@@ -365,6 +365,71 @@ public class FlatWriterRegistryTest {
         }
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testValidateMissingWriter() throws Exception {
+        final FlatWriterRegistry flatWriterRegistry =
+            new FlatWriterRegistry(ImmutableMap.of(DataObject1.IID, writer1));
+
+        final Multimap<InstanceIdentifier<?>, DataObjectUpdate> updates = HashMultimap.create();
+        addUpdate(updates, DataObject1.class);
+        addUpdate(updates, DataObject2.class);
+        flatWriterRegistry.validateModifications(new WriterRegistry.DataObjectUpdates(updates, ImmutableMultimap.of()), ctx);
+    }
+
+    @Test
+    public void testValidateSingleWriter() throws Exception {
+        final FlatWriterRegistry flatWriterRegistry =
+            new FlatWriterRegistry(ImmutableMap.of(DataObject1.IID, writer1, DataObject2.IID, writer2));
+
+        final Multimap<InstanceIdentifier<?>, DataObjectUpdate> updates = HashMultimap.create();
+        final InstanceIdentifier<DataObject1> iid = InstanceIdentifier.create(DataObject1.class);
+        final InstanceIdentifier<DataObject1> iid2 = InstanceIdentifier.create(DataObject1.class);
+        final DataObject1 dataObject = mock(DataObject1.class);
+        updates.put(DataObject1.IID, DataObjectUpdate.create(iid, dataObject, dataObject));
+        updates.put(DataObject1.IID, DataObjectUpdate.create(iid2, dataObject, dataObject));
+        flatWriterRegistry
+            .validateModifications(new WriterRegistry.DataObjectUpdates(updates, ImmutableMultimap.of()), ctx);
+
+        verify(writer1).validate(iid, dataObject, dataObject, ctx);
+        verify(writer1).validate(iid2, dataObject, dataObject, ctx);
+        // Invoked when registry is being created
+        verifyNoMoreInteractions(writer1);
+        verifyZeroInteractions(writer2);
+    }
+
+    @Test
+    public void testValidateMultipleWriters() throws Exception {
+        final FlatWriterRegistry flatWriterRegistry =
+            new FlatWriterRegistry(ImmutableMap.of(DataObject1.IID, writer1, DataObject2.IID, writer2));
+
+        final Multimap<InstanceIdentifier<?>, DataObjectUpdate.DataObjectDelete> deletes = HashMultimap.create();
+        final Multimap<InstanceIdentifier<?>, DataObjectUpdate> updates = HashMultimap.create();
+        final InstanceIdentifier<DataObject1> iid = InstanceIdentifier.create(DataObject1.class);
+        final DataObject1 dataObject = mock(DataObject1.class);
+        // Writer 1 delete
+        deletes.put(DataObject1.IID,
+            ((DataObjectUpdate.DataObjectDelete) DataObjectUpdate.create(iid, dataObject, null)));
+        // Writer 1 create
+        updates.put(DataObject1.IID, DataObjectUpdate.create(iid, null, dataObject));
+        final InstanceIdentifier<DataObject2> iid2 = InstanceIdentifier.create(DataObject2.class);
+        final DataObject2 dataObject2 = mock(DataObject2.class);
+        // Writer 2 delete
+        deletes.put(DataObject2.IID,
+            ((DataObjectUpdate.DataObjectDelete) DataObjectUpdate.create(iid2, dataObject2, null)));
+        // Writer 2 update
+        updates.put(DataObject2.IID, DataObjectUpdate.create(iid2, dataObject2, dataObject2));
+        flatWriterRegistry.validateModifications(new WriterRegistry.DataObjectUpdates(updates, deletes), ctx);
+
+        // Ignore order
+        verify(writer1).validate(iid, dataObject, null, ctx);
+        verify(writer1).validate(iid, null, dataObject, ctx);
+        verify(writer2).validate(iid2, dataObject2, null, ctx);
+        verify(writer2).validate(iid2, dataObject2, dataObject2, ctx);
+
+        verifyNoMoreInteractions(writer1);
+        verifyNoMoreInteractions(writer2);
+    }
+
     private <D extends DataObject> void addKeyedUpdate(final Multimap<InstanceIdentifier<?>, DataObjectUpdate> updates,
                                                        final Class<D> type) throws Exception {
         final InstanceIdentifier<D> iid = (InstanceIdentifier<D>) type.getDeclaredField("IID").get(null);
index 721e2be..67bbc3d 100644 (file)
@@ -77,5 +77,4 @@ public interface WriterCustomizer<D extends DataObject> {
     void deleteCurrentAttributes(@Nonnull final InstanceIdentifier<D> id,
                                  @Nonnull final D dataBefore,
                                  @Nonnull final WriteContext writeContext) throws WriteFailedException;
-
 }
index 4f1b696..ba8f1d9 100644 (file)
 
 package io.fd.honeycomb.translate.util.write;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import io.fd.honeycomb.translate.util.RWUtils;
+import io.fd.honeycomb.translate.write.DataValidationFailedException;
+import io.fd.honeycomb.translate.write.Validator;
 import io.fd.honeycomb.translate.write.WriteContext;
 import io.fd.honeycomb.translate.write.WriteFailedException;
 import io.fd.honeycomb.translate.write.Writer;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
 public abstract class AbstractGenericWriter<D extends DataObject> implements Writer<D> {
 
     private static final Logger LOG = LoggerFactory.getLogger(AbstractGenericWriter.class);
 
     private final InstanceIdentifier<D> instanceIdentifier;
     private final boolean supportsUpdate;
+    private Validator<D> validator;
 
     protected AbstractGenericWriter(final InstanceIdentifier<D> type, final boolean supportsUpdate) {
         this.instanceIdentifier = RWUtils.makeIidWildcarded(type);
         this.supportsUpdate = supportsUpdate;
     }
 
+    protected AbstractGenericWriter(final InstanceIdentifier<D> type, final boolean supportsUpdate,
+                                    final Validator<D> validator) {
+        this(type, supportsUpdate);
+        this.validator = validator;
+    }
+
     protected void writeCurrent(final InstanceIdentifier<D> id, final D data, final WriteContext ctx)
         throws WriteFailedException {
         LOG.debug("{}: Writing current: {} data: {}", this, id, data);
@@ -90,6 +98,42 @@ public abstract class AbstractGenericWriter<D extends DataObject> implements Wri
         }
     }
 
+    @Override
+    public void validate(@Nonnull final InstanceIdentifier<? extends DataObject> id,
+                         @Nullable final DataObject dataBefore, @Nullable final DataObject dataAfter,
+                         @Nonnull final WriteContext ctx) throws DataValidationFailedException {
+        if (validator == null) {
+            LOG.trace("{}: validator is not defined. Skipping validation for {}", this, id);
+            return;
+        }
+        LOG.trace("{}: Validating : {}, before: {} after: {}", this, id, dataBefore, dataAfter);
+
+        checkArgument(idPointsToCurrent(id), "Cannot handle data: %s. Only: %s can be handled by writer: %s",
+            id, getManagedDataObjectType(), this);
+
+        if (isWrite(dataBefore, dataAfter)) {
+            final D after = castToManaged(dataAfter);
+            validator.validateWrite(getManagedId(id, after), after, ctx);
+        } else if (isDelete(dataBefore, dataAfter)) {
+            final D before = castToManaged(dataBefore);
+            validator.validateDelete(getManagedId(id, before), before, ctx);
+        } else {
+            checkArgument(dataBefore != null && dataAfter != null, "No data to process");
+            if (dataBefore.equals(dataAfter)) {
+                LOG.debug("{}: Skipping validation (no update) for: {}", this, id);
+                // No change, ignore
+                return;
+            }
+            final D before = castToManaged(dataBefore);
+            validator.validateUpdate(getManagedId(id, before), before, castToManaged(dataAfter), ctx);
+        }
+    }
+
+    protected InstanceIdentifier<D> getManagedId(@Nonnull final InstanceIdentifier<? extends DataObject> currentId,
+                                                 @Nonnull final D current) {
+        return (InstanceIdentifier<D>) currentId;
+    }
+
     private void checkDataType(@Nonnull final DataObject dataAfter) {
         checkArgument(getManagedDataObjectType().getTargetType().isAssignableFrom(dataAfter.getClass()));
     }