[ONOS-7732] Automating switch workflow: annotation based data model injection, applying programming counter, and small fixes

Change-Id: I4092d9c2695bcc8c4e8e01d54c442d3fac284eb6
diff --git a/apps/workflow/api/src/main/java/org/onosproject/workflow/api/JsonDataModelInjector.java b/apps/workflow/api/src/main/java/org/onosproject/workflow/api/JsonDataModelInjector.java
new file mode 100644
index 0000000..d76aa52
--- /dev/null
+++ b/apps/workflow/api/src/main/java/org/onosproject/workflow/api/JsonDataModelInjector.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.workflow.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.MissingNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Class for injecting json data model on the work-let execution context.
+ */
+public class JsonDataModelInjector {
+
+    private static final Logger log = LoggerFactory.getLogger(JsonDataModelInjector.class);
+
+    /**
+     * Injects data model to work-let.
+     * @param worklet work-let to be injected
+     * @param context workflow context
+     * @throws WorkflowException workflow exception
+     */
+    public void inject(Worklet worklet, WorkflowContext context) throws WorkflowException {
+
+        handle(worklet, context, this::injectModel);
+    }
+
+    /**
+     * Inhales data model from work-let.
+     * @param worklet work-let to be inhaled
+     * @param context workflow context
+     * @throws WorkflowException workflow exception
+     */
+    public void inhale(Worklet worklet, WorkflowContext context) throws WorkflowException {
+
+        handle(worklet, context, this::inhaleModel);
+    }
+
+    private void handle(Worklet worklet, WorkflowContext context, DataModelFieldBehavior func)
+            throws WorkflowException {
+        Class cl = worklet.getClass();
+        List<Field> fields = getInheritedFields(cl);
+        if (Objects.isNull(fields)) {
+            log.error("Invalid fields on {}", cl);
+            return;
+        }
+
+        for (Field field: fields) {
+            Annotation[] annotations = field.getAnnotations();
+            if (Objects.isNull(annotations)) {
+                continue;
+            }
+            for (Annotation annotation: annotations) {
+                if (!(annotation instanceof JsonDataModel)) {
+                    continue;
+                }
+                JsonDataModel model = (JsonDataModel) annotation;
+                func.apply(worklet, context, field, model);
+            }
+        }
+    }
+
+    private static List<Field> getInheritedFields(Class<?> type) {
+        List<Field> fields = new ArrayList<Field>();
+
+        Class<?> cl = type;
+        while (cl != null && cl != Object.class) {
+            for (Field field : cl.getDeclaredFields()) {
+                if (!field.isSynthetic()) {
+                    fields.add(field);
+                }
+            }
+            cl = cl.getSuperclass();
+        }
+        return fields;
+    }
+
+    /**
+     * Functional interface for json data model annotated field behavior.
+     */
+    @FunctionalInterface
+    public interface DataModelFieldBehavior {
+        void apply(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+                throws WorkflowException;
+    }
+
+    private static Map<Class, DataModelFieldBehavior> injectTypeMap = new HashMap<>();
+    static {
+        injectTypeMap.put(String.class, JsonDataModelInjector::injectText);
+        injectTypeMap.put(Integer.class, JsonDataModelInjector::injectInteger);
+        injectTypeMap.put(Boolean.class, JsonDataModelInjector::injectBoolean);
+        injectTypeMap.put(JsonNode.class, JsonDataModelInjector::injectJsonNode);
+        injectTypeMap.put(ArrayNode.class, JsonDataModelInjector::injectArrayNode);
+        injectTypeMap.put(ObjectNode.class, JsonDataModelInjector::injectObjectNode);
+    }
+
+    /**
+     * Injects data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private void injectModel(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        DataModelFieldBehavior behavior = injectTypeMap.get(model.type());
+        if (Objects.isNull(behavior)) {
+            throw new WorkflowException("Not supported type(" + model.type() + ")");
+        }
+        behavior.apply(worklet, context, field, model);
+    }
+
+    /**
+     * Injects text data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model text data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void injectText(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        String text = ((JsonDataModelTree) context.data()).textAt(model.path());
+        if (Objects.isNull(text)) {
+            if (model.optional()) {
+                return;
+            }
+            throw new WorkflowException("Invalid text data model on (" + model.path() + ")");
+        }
+
+        if (!(Objects.equals(field.getType(), String.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not String");
+        }
+
+        try {
+            field.setAccessible(true);
+            field.set(worklet, text);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+    }
+
+    /**
+     * Injects integer data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model integer data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void injectInteger(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        Integer number = ((JsonDataModelTree) context.data()).intAt(model.path());
+        if (Objects.isNull(number)) {
+            if (model.optional()) {
+                return;
+            }
+            throw new WorkflowException("Invalid number data model on (" + model.path() + ")");
+        }
+
+        if (!(Objects.equals(field.getType(), Integer.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not Integer");
+        }
+
+        try {
+            field.setAccessible(true);
+            field.set(worklet, number);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+    }
+
+    /**
+     * Injects boolean data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model boolean data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void injectBoolean(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        Boolean bool = ((JsonDataModelTree) context.data()).booleanAt(model.path());
+        if (Objects.isNull(bool)) {
+            if (model.optional()) {
+                return;
+            }
+            throw new WorkflowException("Invalid boolean data model on (" + model.path() + ")");
+        }
+
+        if (!(Objects.equals(field.getType(), Boolean.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not Boolean");
+        }
+
+        try {
+            field.setAccessible(true);
+            field.set(worklet, bool);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+    }
+
+    /**
+     * Injects json node data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model json node data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void injectJsonNode(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        JsonNode jsonNode = ((JsonDataModelTree) context.data()).nodeAt(model.path());
+        if (Objects.isNull(jsonNode)) {
+            if (model.optional()) {
+                return;
+            }
+            throw new WorkflowException("Invalid json node data model on (" + model.path() + ")");
+        }
+
+        if (!(Objects.equals(field.getType(), JsonNode.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not JsonNode");
+        }
+
+        try {
+            field.setAccessible(true);
+            field.set(worklet, jsonNode);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+    }
+
+    /**
+     * Injects json array node data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model json array node data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void injectArrayNode(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        ArrayNode arrayNode = ((JsonDataModelTree) context.data()).arrayAt(model.path());
+        if (Objects.isNull(arrayNode)) {
+            if (model.optional()) {
+                return;
+            }
+            throw new WorkflowException("Invalid array node data model on (" + model.path() + ")");
+        }
+
+        if (!(Objects.equals(field.getType(), ArrayNode.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not ArrayNode");
+        }
+
+        try {
+            field.setAccessible(true);
+            field.set(worklet, arrayNode);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+    }
+
+    /**
+     * Injects json object node data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model json object node data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void injectObjectNode(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        ObjectNode objNode = ((JsonDataModelTree) context.data()).objectAt(model.path());
+        if (Objects.isNull(objNode)) {
+            if (model.optional()) {
+                return;
+            }
+            throw new WorkflowException("Invalid object node data model on (" + model.path() + ")");
+        }
+
+        if (!(Objects.equals(field.getType(), ObjectNode.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not ObjectNode");
+        }
+
+        try {
+            field.setAccessible(true);
+            field.set(worklet, objNode);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+    }
+
+    private static Map<Class, DataModelFieldBehavior> inhaleTypeMap = new HashMap<>();
+    static {
+        inhaleTypeMap.put(String.class, JsonDataModelInjector::inhaleText);
+        inhaleTypeMap.put(Integer.class, JsonDataModelInjector::inhaleInteger);
+        inhaleTypeMap.put(Boolean.class, JsonDataModelInjector::inhaleBoolean);
+        inhaleTypeMap.put(JsonNode.class, JsonDataModelInjector::inhaleJsonNode);
+        inhaleTypeMap.put(ArrayNode.class, JsonDataModelInjector::inhaleArrayNode);
+        inhaleTypeMap.put(ObjectNode.class, JsonDataModelInjector::inhaleObjectNode);
+    }
+
+    /**
+     * Inhales data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private void inhaleModel(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        DataModelFieldBehavior behavior = inhaleTypeMap.get(model.type());
+        if (Objects.isNull(behavior)) {
+            throw new WorkflowException("Not supported type(" + model.type() + ")");
+        }
+        behavior.apply(worklet, context, field, model);
+    }
+
+    /**
+     * Inhales text data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model text data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void inhaleText(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        if (!(Objects.equals(field.getType(), String.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not String");
+        }
+
+        String text;
+        try {
+            field.setAccessible(true);
+            text = (String) field.get(worklet);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+
+        if (Objects.isNull(text)) {
+            return;
+        }
+
+        JsonDataModelTree tree = (JsonDataModelTree) context.data();
+        JsonNode jsonNode = tree.nodeAt(model.path());
+
+        if (Objects.isNull(jsonNode) || jsonNode instanceof MissingNode) {
+            tree.setAt(model.path(), text);
+        } else if (!(jsonNode instanceof TextNode)) {
+            throw new WorkflowException("Invalid text data model on (" + model.path() + ")");
+        } else {
+            tree.remove(model.path());
+            tree.setAt(model.path(), text);
+        }
+    }
+
+    /**
+     * Inhales integer data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model integer data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void inhaleInteger(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        if (!(Objects.equals(field.getType(), Integer.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not Integer");
+        }
+
+        Integer number;
+        try {
+            field.setAccessible(true);
+            number = (Integer) field.get(worklet);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+
+        if (Objects.isNull(number)) {
+            return;
+        }
+
+        JsonDataModelTree tree = (JsonDataModelTree) context.data();
+        JsonNode jsonNode = tree.nodeAt(model.path());
+
+        if (Objects.isNull(jsonNode) || jsonNode instanceof MissingNode) {
+            tree.setAt(model.path(), number);
+        } else if (!(jsonNode instanceof IntNode)) {
+            throw new WorkflowException("Invalid integer data model on (" + model.path() + ")");
+        } else {
+            tree.remove(model.path());
+            tree.setAt(model.path(), number);
+        }
+    }
+
+    /**
+     * Inhales boolean data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model boolean data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void inhaleBoolean(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        if (!(Objects.equals(field.getType(), Boolean.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not Boolean");
+        }
+
+        Boolean bool;
+        try {
+            field.setAccessible(true);
+            bool = (Boolean) field.get(worklet);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+
+        if (Objects.isNull(bool)) {
+            return;
+        }
+
+        JsonDataModelTree tree = (JsonDataModelTree) context.data();
+        JsonNode jsonNode = tree.nodeAt(model.path());
+
+        if (Objects.isNull(jsonNode) || jsonNode instanceof MissingNode) {
+            tree.setAt(model.path(), bool);
+        } else if (!(jsonNode instanceof BooleanNode)) {
+            throw new WorkflowException("Invalid boolean data model on (" + model.path() + ")");
+        } else {
+            tree.remove(model.path());
+            tree.setAt(model.path(), bool);
+        }
+    }
+
+    /**
+     * Inhales json node data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model json node data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void inhaleJsonNode(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+
+        if (!(Objects.equals(field.getType(), JsonNode.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not JsonNode");
+        }
+
+        JsonNode tgtJsonNode;
+        try {
+            field.setAccessible(true);
+            tgtJsonNode = (JsonNode) field.get(worklet);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+
+        if (Objects.isNull(tgtJsonNode)) {
+            return;
+        }
+
+        JsonDataModelTree tree = (JsonDataModelTree) context.data();
+        JsonNode jsonNode = tree.nodeAt(model.path());
+
+        if (Objects.isNull(jsonNode) || jsonNode instanceof MissingNode) {
+            tree.attach(model.path(), new JsonDataModelTree(tgtJsonNode));
+        } else if (!(jsonNode instanceof JsonNode)) {
+            throw new WorkflowException("Invalid json node data model on (" + model.path() + ")");
+        } else {
+            // do nothing
+        }
+    }
+
+    /**
+     * Inhales json array node data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model json array node data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void inhaleArrayNode(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+        if (!(Objects.equals(field.getType(), ArrayNode.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not ArrayNode");
+        }
+
+        ArrayNode tgtArrayNode;
+        try {
+            field.setAccessible(true);
+            tgtArrayNode = (ArrayNode) field.get(worklet);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+
+        if (Objects.isNull(tgtArrayNode)) {
+            return;
+        }
+
+        JsonDataModelTree tree = (JsonDataModelTree) context.data();
+        JsonNode jsonNode = tree.nodeAt(model.path());
+
+        if (Objects.isNull(jsonNode) || jsonNode instanceof MissingNode) {
+            tree.attach(model.path(), new JsonDataModelTree(tgtArrayNode));
+        } else if (!(jsonNode instanceof ArrayNode)) {
+            throw new WorkflowException("Invalid array node data model on (" + model.path() + ")");
+        } else {
+            // do nothing
+        }
+    }
+
+    /**
+     * Inhales json object node data model on the filed of work-let.
+     * @param worklet work-let
+     * @param context workflow context
+     * @param field the field of work-let
+     * @param model json object node data model for the field
+     * @throws WorkflowException workflow exception
+     */
+    private static void inhaleObjectNode(Worklet worklet, WorkflowContext context, Field field, JsonDataModel model)
+            throws WorkflowException {
+        if (!(Objects.equals(field.getType(), ObjectNode.class))) {
+            throw new WorkflowException("Target field (" + field + ") is not ObjectNode");
+        }
+
+        ObjectNode tgtObjNode;
+        try {
+            field.setAccessible(true);
+            tgtObjNode = (ObjectNode) field.get(worklet);
+        } catch (IllegalAccessException e) {
+            throw new WorkflowException(e);
+        }
+
+        if (Objects.isNull(tgtObjNode)) {
+            return;
+        }
+
+        JsonDataModelTree tree = (JsonDataModelTree) context.data();
+        JsonNode jsonNode = tree.nodeAt(model.path());
+
+        if (Objects.isNull(jsonNode) || jsonNode instanceof MissingNode) {
+            tree.attach(model.path(), new JsonDataModelTree(tgtObjNode));
+        } else if (!(jsonNode instanceof ObjectNode)) {
+            throw new WorkflowException("Invalid object node data model on (" + model.path() + ")");
+        } else {
+            // do nothing
+        }
+    }
+}