ONOS-3387 Adding ability for network configurations to be validated before being accepted into the system.

Change-Id: I26a7e2adb20318cf17a35081ff753b3448105e31
diff --git a/core/api/src/main/java/org/onosproject/net/config/Config.java b/core/api/src/main/java/org/onosproject/net/config/Config.java
index 5cdc0c1..3757d32 100644
--- a/core/api/src/main/java/org/onosproject/net/config/Config.java
+++ b/core/api/src/main/java/org/onosproject/net/config/Config.java
@@ -20,10 +20,15 @@
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Function;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -51,6 +56,21 @@
     protected ConfigApplyDelegate delegate;
 
     /**
+     * Indicator of whether a configuration JSON field is required.
+     */
+    public enum FieldPresence {
+        /**
+         * Signifies that config field is an optional one.
+         */
+        OPTIONAL,
+
+        /**
+         * Signifies that config field is mandatory.
+         */
+        MANDATORY
+    }
+
+    /**
      * Initializes the configuration behaviour with necessary context.
      *
      * @param subject  configuration subject
@@ -71,6 +91,29 @@
     }
 
     /**
+     * Indicates whether or not the backing JSON node contains valid data.
+     * <p>
+     * Default implementation returns true.
+     * Subclasses are expected to override this with their own validation.
+     * </p>
+     *
+     * @return true if the data is valid; false otherwise
+     */
+    public boolean isValid() {
+        // TODO: figure out what assertions could be made in the base class
+        // NOTE: The thought is to have none, but instead to provide a set
+        // of predicates to allow configs to test validity of present fields,
+        // e.g.:
+        //      isString(path)
+        //      isBoolean(path)
+        //      isNumber(path, [min, max])
+        //      isDecimal(path, [min, max])
+        //      isMacAddress(path)
+        //      isIpAddress(path)
+        return true;
+    }
+
+    /**
      * Returns the specific subject to which this configuration pertains.
      *
      * @return configuration subject
@@ -309,4 +352,104 @@
         return this;
     }
 
+    /**
+     * Indicates whether only the specified fields are present in the backing JSON.
+     *
+     * @param allowedFields allowed field names
+     * @return true if all allowedFields are present; false otherwise
+     */
+    protected boolean hasOnlyFields(String... allowedFields) {
+        Set<String> fields = ImmutableSet.copyOf(allowedFields);
+        return !Iterators.any(object.fieldNames(), f -> !fields.contains(f));
+    }
+
+    /**
+     * Indicates whether the specified field holds a valid MAC address.
+     *
+     * @param field    JSON field name
+     * @param presence specifies if field is optional or mandatory
+     * @return true if valid; false otherwise
+     * @throws IllegalArgumentException if field is present, but not valid MAC
+     */
+    protected boolean isMacAddress(String field, FieldPresence presence) {
+        JsonNode node = object.path(field);
+        return isValid(node, presence, node.isTextual() &&
+                MacAddress.valueOf(node.asText()) != null);
+    }
+
+    /**
+     * Indicates whether the specified field holds a valid IP address.
+     *
+     * @param field    JSON field name
+     * @param presence specifies if field is optional or mandatory
+     * @return true if valid; false otherwise
+     * @throws IllegalArgumentException if field is present, but not valid IP
+     */
+    protected boolean isIpAddress(String field, FieldPresence presence) {
+        JsonNode node = object.path(field);
+        return isValid(node, presence, node.isTextual() &&
+                IpAddress.valueOf(node.asText()) != null);
+    }
+
+    /**
+     * Indicates whether the specified field holds a valid string value.
+     *
+     * @param field    JSON field name
+     * @param presence specifies if field is optional or mandatory
+     * @param pattern  optional regex pattern
+     * @return true if valid; false otherwise
+     * @throws IllegalArgumentException if field is present, but not valid MAC
+     */
+    protected boolean isString(String field, FieldPresence presence, String... pattern) {
+        JsonNode node = object.path(field);
+        return isValid(node, presence, node.isTextual() &&
+                (pattern.length > 0 && node.asText().matches(pattern[0]) || pattern.length < 1));
+    }
+
+    /**
+     * Indicates whether the specified field holds a valid number.
+     *
+     * @param field    JSON field name
+     * @param presence specifies if field is optional or mandatory
+     * @param minMax   optional min/max values
+     * @return true if valid; false otherwise
+     * @throws IllegalArgumentException if field is present, but not valid
+     */
+    protected boolean isNumber(String field, FieldPresence presence, long... minMax) {
+        JsonNode node = object.path(field);
+        return isValid(node, presence, (node.isLong() || node.isInt()) &&
+                (minMax.length > 0 && minMax[0] <= node.asLong() || minMax.length < 1) &&
+                (minMax.length > 1 && minMax[1] > node.asLong() || minMax.length < 2));
+    }
+
+    /**
+     * Indicates whether the specified field holds a valid decimal number.
+     *
+     * @param field    JSON field name
+     * @param presence specifies if field is optional or mandatory
+     * @param minMax   optional min/max values
+     * @return true if valid; false otherwise
+     * @throws IllegalArgumentException if field is present, but not valid
+     */
+    protected boolean isDecimal(String field, FieldPresence presence, double... minMax) {
+        JsonNode node = object.path(field);
+        return isValid(node, presence, (node.isDouble() || node.isFloat()) &&
+                (minMax.length > 0 && minMax[0] <= node.asDouble() || minMax.length < 1) &&
+                (minMax.length > 1 && minMax[1] > node.asDouble() || minMax.length < 2));
+    }
+
+    /**
+     * Indicates whether the node is present and of correct value or not
+     * mandatory and absent.
+     *
+     * @param node         JSON node
+     * @param presence     specifies if field is optional or mandatory
+     * @param correctValue true if the value is correct
+     * @return true if the field is as expected
+     */
+    private boolean isValid(JsonNode node, FieldPresence presence, boolean correctValue) {
+        boolean isMandatory = presence == FieldPresence.MANDATORY;
+        return isMandatory && correctValue || !isMandatory && !node.isNull() || correctValue;
+    }
+
 }