STC work in progress

Change-Id: Ie5e444e3b560b605b066899289cdee7a5fe8338c
diff --git a/utils/stc/src/main/java/org/onlab/stc/Compiler.java b/utils/stc/src/main/java/org/onlab/stc/Compiler.java
new file mode 100644
index 0000000..6ee03c5
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Compiler.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.onlab.stc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Entity responsible for loading a scenario and producing a redy-to-execute
+ * process flow graph.
+ */
+public class Compiler {
+
+    private static final String DEFAULT_LOG_DIR = "${env.WORKSPACE}/tmp/stc/";
+
+    private static final String IMPORT = "import";
+    private static final String GROUP = "group";
+    private static final String STEP = "step";
+    private static final String PARALLEL = "parallel";
+    private static final String DEPENDENCY = "dependency";
+
+    private static final String LOG_DIR = "[@logDir]";
+    private static final String NAME = "[@name]";
+    private static final String COMMAND = "[@exec]";
+    private static final String REQUIRES = "[@requires]";
+    private static final String IF = "[@if]";
+    private static final String UNLESS = "[@unless]";
+    private static final String VAR = "[@var]";
+    private static final String FILE = "[@file]";
+    private static final String NAMESPACE = "[@namespace]";
+
+    private static final String PROP_START = "${";
+    private static final String PROP_END = "}";
+    private static final String HASH = "#";
+
+    private final Scenario scenario;
+
+    private final Map<String, Step> steps = Maps.newHashMap();
+    private final Map<String, Step> inactiveSteps = Maps.newHashMap();
+    private final Set<Dependency> dependencies = Sets.newHashSet();
+    private final List<Integer> parallels = Lists.newArrayList();
+
+    private ProcessFlow processFlow;
+    private File logDir;
+
+    private String pfx = "";
+    private boolean debugOn = System.getenv("debug") != null;
+
+    /**
+     * Creates a new compiler for the specified scenario.
+     *
+     * @param scenario scenario to be compiled
+     */
+    public Compiler(Scenario scenario) {
+        this.scenario = scenario;
+    }
+
+    /**
+     * Returns the scenario being compiled.
+     *
+     * @return test scenario
+     */
+    public Scenario scenario() {
+        return scenario;
+    }
+
+    /**
+     * Compiles the specified scenario to produce a final process flow graph.
+     */
+    public void compile() {
+        compile(scenario.definition(), null, null);
+
+        // Produce the process flow
+        processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
+                                      ImmutableSet.copyOf(dependencies));
+
+        // Extract the log directory if there was one specified
+        String path = scenario.definition().getString(LOG_DIR, DEFAULT_LOG_DIR);
+        logDir = new File(expand(path));
+    }
+
+    /**
+     * Returns the step with the specified name.
+     *
+     * @param name step or group name
+     * @return test step or group
+     */
+    public Step getStep(String name) {
+        return steps.get(name);
+    }
+
+    /**
+     * Returns the process flow generated from this scenario definition.
+     *
+     * @return process flow as a graph
+     */
+    public ProcessFlow processFlow() {
+        return processFlow;
+    }
+
+    /**
+     * Returns the log directory where scenario logs should be kept.
+     * @return scenario logs directory
+     */
+    public File logDir() {
+        return logDir;
+    }
+
+    /**
+     * Recursively elaborates this definition to produce a final process flow graph.
+     *
+     * @param cfg         hierarchical definition
+     * @param namespace   optional namespace
+     * @param parentGroup optional parent group
+     */
+    private void compile(HierarchicalConfiguration cfg,
+                         String namespace, Group parentGroup) {
+        String opfx = pfx;
+        pfx = pfx + ">";
+
+        // Scan all imports
+        cfg.configurationsAt(IMPORT)
+                .forEach(c -> processImport(c, namespace, parentGroup));
+
+        // Scan all steps
+        cfg.configurationsAt(STEP)
+                .forEach(c -> processStep(c, namespace, parentGroup));
+
+        // Scan all groups
+        cfg.configurationsAt(GROUP)
+                .forEach(c -> processGroup(c, namespace, parentGroup));
+
+        // Scan all parallel groups
+        cfg.configurationsAt(PARALLEL)
+                .forEach(c -> processParallelGroup(c, namespace, parentGroup));
+
+        // Scan all dependencies
+        cfg.configurationsAt(DEPENDENCY)
+                .forEach(c -> processDependency(c, namespace));
+
+        pfx = opfx;
+    }
+
+    /**
+     * Processes an import directive.
+     *
+     * @param cfg         hierarchical definition
+     * @param namespace   optional namespace
+     * @param parentGroup optional parent group
+     */
+    private void processImport(HierarchicalConfiguration cfg,
+                               String namespace, Group parentGroup) {
+        String file = checkNotNull(expand(cfg.getString(FILE)),
+                                   "Import directive must specify 'file'");
+        String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
+        print("import file=%s namespace=%s", file, newNamespace);
+        try {
+            Scenario importScenario = loadScenario(new FileInputStream(file));
+            compile(importScenario.definition(), namespace, parentGroup);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Unable to import scenario", e);
+        }
+    }
+
+    /**
+     * Processes a step directive.
+     *
+     * @param cfg         hierarchical definition
+     * @param namespace   optional namespace
+     * @param parentGroup optional parent group
+     */
+    private void processStep(HierarchicalConfiguration cfg,
+                             String namespace, Group parentGroup) {
+        String name = expand(prefix(cfg.getString(NAME), namespace));
+        String defaultValue = parentGroup != null ? parentGroup.command() : null;
+        String command = expand(cfg.getString(COMMAND, defaultValue));
+
+        print("step name=%s command=%s", name, command);
+        Step step = new Step(name, command, parentGroup);
+        registerStep(step, cfg, namespace, parentGroup);
+    }
+
+    /**
+     * Processes a group directive.
+     *
+     * @param cfg         hierarchical definition
+     * @param namespace   optional namespace
+     * @param parentGroup optional parent group
+     */
+    private void processGroup(HierarchicalConfiguration cfg,
+                              String namespace, Group parentGroup) {
+        String name = expand(prefix(cfg.getString(NAME), namespace));
+        String defaultValue = parentGroup != null ? parentGroup.command() : null;
+        String command = expand(cfg.getString(COMMAND, defaultValue));
+
+        print("group name=%s command=%s", name, command);
+        Group group = new Group(name, command, parentGroup);
+        if (registerStep(group, cfg, namespace, parentGroup)) {
+            compile(cfg, namespace, group);
+        }
+    }
+
+    /**
+     * Registers the specified step or group.
+     *
+     * @param step        step or group
+     * @param cfg         hierarchical definition
+     * @param namespace   optional namespace
+     * @param parentGroup optional parent group
+     * @return true of the step or group was registered as active
+     */
+    private boolean registerStep(Step step, HierarchicalConfiguration cfg,
+                                 String namespace, Group parentGroup) {
+        String ifClause = expand(cfg.getString(IF));
+        String unlessClause = expand(cfg.getString(UNLESS));
+
+        if ((ifClause != null && ifClause.length() == 0) ||
+                (unlessClause != null && unlessClause.length() > 0) ||
+                (parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
+            inactiveSteps.put(step.name(), step);
+            return false;
+        }
+
+        if (parentGroup != null) {
+            parentGroup.addChild(step);
+        }
+        steps.put(step.name(), step);
+        processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
+        return true;
+    }
+
+    /**
+     * Processes a parallel clone group directive.
+     *
+     * @param cfg         hierarchical definition
+     * @param namespace   optional namespace
+     * @param parentGroup optional parent group
+     */
+    private void processParallelGroup(HierarchicalConfiguration cfg,
+                                      String namespace, Group parentGroup) {
+        String var = cfg.getString(VAR);
+        print("parallel var=%s", var);
+
+        int i = 1;
+        while (condition(var, i).length() > 0) {
+            parallels.add(0, i);
+            compile(cfg, namespace, parentGroup);
+            parallels.remove(0);
+            i++;
+        }
+    }
+
+    /**
+     * Returns the elaborated repetition construct conditional.
+     *
+     * @param var repetition var property
+     * @param i   index to elaborate
+     * @return elaborated string
+     */
+    private String condition(String var, Integer i) {
+        return expand(var.replaceFirst("#", i.toString())).trim();
+    }
+
+    /**
+     * Processes a dependency directive.
+     *
+     * @param cfg       hierarchical definition
+     * @param namespace optional namespace
+     */
+    private void processDependency(HierarchicalConfiguration cfg, String namespace) {
+        String name = expand(prefix(cfg.getString(NAME), namespace));
+        String requires = expand(cfg.getString(REQUIRES));
+
+        print("dependency name=%s requires=%s", name, requires);
+        Step step = getStep(name, namespace);
+        processRequirements(step, requires, namespace);
+    }
+
+    /**
+     * Processes the specified requiremenst string and adds dependency for
+     * each requirement of the given step.
+     *
+     * @param src       source step
+     * @param requires  comma-separated list of required steps
+     * @param namespace optional namespace
+     */
+    private void processRequirements(Step src, String requires, String namespace) {
+        split(requires).forEach(name -> {
+            boolean isSoft = name.startsWith("-");
+            Step dst = getStep(expand(name.replaceFirst("^-", "")), namespace);
+            if (!inactiveSteps.containsValue(dst)) {
+                dependencies.add(new Dependency(src, dst, isSoft));
+            }
+        });
+    }
+
+    /**
+     * Retrieves the step or group with the specified name.
+     *
+     * @param name      step or group name
+     * @param namespace optional namespace
+     * @return step or group; null if none found in active or inactive steps
+     */
+    private Step getStep(String name, String namespace) {
+        String dName = prefix(name, namespace);
+        Step step = steps.get(dName);
+        step = step != null ? step : inactiveSteps.get(dName);
+        checkArgument(step != null, "Unknown step %s", dName);
+        return step;
+    }
+
+    /**
+     * Prefixes the specified name with the given namespace.
+     *
+     * @param name      name of a step or a group
+     * @param namespace optional namespace
+     * @return composite name
+     */
+    private String prefix(String name, String namespace) {
+        return namespace != null ? namespace + "." + name : name;
+    }
+
+    /**
+     * Expands any environment variables in the specified
+     * string. These are specified as ${property} tokens.
+     *
+     * @param string string to be processed
+     * @return original string with expanded substitutions
+     */
+    private String expand(String string) {
+        if (string == null) {
+            return null;
+        }
+
+        String pString = string;
+        StringBuilder sb = new StringBuilder();
+        int start, end, last = 0;
+        while ((start = pString.indexOf(PROP_START, last)) >= 0) {
+            end = pString.indexOf(PROP_END, start + PROP_START.length());
+            checkArgument(end > start, "Malformed property in %s", pString);
+            sb.append(pString.substring(last, start));
+            String prop = pString.substring(start + PROP_START.length(), end);
+            String value;
+            if (prop.equals(HASH)) {
+                value = parallels.get(0).toString();
+            } else if (prop.endsWith(HASH)) {
+                pString = pString.replaceFirst("#}", parallels.get(0).toString() + "}");
+                last = start;
+                continue;
+            } else {
+                // Try system property first, then fall back to env. variable.
+                value = System.getProperty(prop);
+                if (value == null) {
+                    value = System.getenv(prop);
+                }
+            }
+            sb.append(value != null ? value : "");
+            last = end + 1;
+        }
+        sb.append(pString.substring(last));
+        return sb.toString();
+    }
+
+    /**
+     * Splits the comma-separated string into a list of strings.
+     *
+     * @param string string to split
+     * @return list of strings
+     */
+    private List<String> split(String string) {
+        ImmutableList.Builder<String> builder = ImmutableList.builder();
+        String[] fields = string != null ? string.split(",") : new String[0];
+        for (String field : fields) {
+            builder.add(field.trim());
+        }
+        return builder.build();
+    }
+
+    /**
+     * Prints formatted output.
+     *
+     * @param format printf format string
+     * @param args   arguments to be printed
+     */
+    private void print(String format, Object... args) {
+        if (debugOn) {
+            System.err.println(pfx + String.format(format, args));
+        }
+    }
+
+}