org.qcert.camp.translator.Rule2CAMP.java Source code

Java tutorial

Introduction

Here is the source code for org.qcert.camp.translator.Rule2CAMP.java

Source

/**
 * (c) Copyright IBM Corp. 2015-2017
 * Copyright (C) 2017 Joshua Auerbach 
 *
 * 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.qcert.camp.translator;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;

import org.qcert.camp.pattern.CampPattern;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonWriter;
import com.ibm.rules.engine.ruledef.runtime.RuleEngine;
import com.ibm.rules.engine.ruledef.runtime.RuleEngineInput;
import com.ibm.rules.engine.util.EngineExecutionException;
import com.ibm.rules.engine.util.EngineInvalidStateException;

/**
 * Utility main program for translating various kinds of "jrules" (ODM rule languages) to CAMP
  */
public class Rule2CAMP {
    private Rule2CAMP() {
    }

    /**
     * Main program.  At least one argument must be specified.
     * <ul>
     * <li><b>-output</b> <em>directory</em> (optional) specifies where <b>.camp</b> and/or <b>.io</b> files should be placed.  
     * Otherwise, they go in the same directory as the corresponding source files (for those rules that are specified as files).
     * For rules specified as "designer links", the default output location is the current directory.
     * 
     * <li><b>-input</b> <em>file path</em> (optional) provides execution input.  If specified, the rule is executed with an ODM
     *  rule engine using the specified input as working memory and a <b.io</b> file is generated to contain the input and schema along
     *  with specific output generated by the rule.  This can be employed in a correctness test when the same rule is compiled by
     *  qcert and executed by a backend that uses a compatible input format (e.g. Javascript).  Use of this option also requires that 
     *  the types used in the schema are present as classes in the classpath with public fields of appropriate type for the attributes.
     *  
     * <li><b>-nocamp</b> (optional) is useful only when <b>-input</b> is also specified.  It suppresses the generating of a <b>.camp</b> file so
     * that the <b>.io</b> file (if any) is the sole output from each rule.
     * 
     * <li><b>-workspace</b> <em>directory</em> specifies the parent directory of all designer projects referenced in designer links.
     * Required if designer links are used, ignored otherwise.
     * 
     * <li><b>-schema</b> <em>file path</em> provides the schema information (in JSON) for processing .arl file arguments. Ignored for .sem file arguments
     *   and designer links.  File path must be fully qualified or relative to the current directory.
     *   
     * <li>A valid file name ending in <b>.arl</b> specifies a technical rule stored in a file in the language we have generally called
     * ARL although the meaning of the terms ARL, IRL, and TRL across ODM Rules and Insights is a little unclear.
     * 
     * <li>A valid file name ending in <b>.sem</b> specifies a serialized <b>SemRule</b> produced by one of the ODM Designer tools.
     * 
     * <li>An argument containing a sequence of two colons, as in <b>Test::tests.TestRule</b> is a "designer link."  The portion before the 
     * colons is the name of a rule project, and the balance denotes a rule in that project using an optionally qualified name.  
     * The last or only name segment is the name of a rule.  Name segments before the last period
     * (if any) form the name of a package containing the rule.  If the rule name contains blanks (as is conventional in ODM), the
     * entire argument or at least the blank-containing segment must be quoted or the blanks must be escaped.
     * 
     * <li>All other arguments are illegal.
     * </ul>
     * @throws Exception 
     */
    public static void main(String[] args) throws Exception {

        /* Parse arguments */
        String workspace = null, outputDir = null, schemaFile = null, inputFile = null;
        boolean workspacePending = false, outputPending = false, schemaPending = false, inputPending = false;
        boolean suppressCamp = false;
        List<String> ordinaryArguments = new ArrayList<>();
        for (String arg : args) {
            if (outputPending) {
                outputDir = arg;
                outputPending = false;
            } else if (workspacePending) {
                workspace = arg;
                workspacePending = false;
            } else if (schemaPending) {
                schemaFile = arg;
                schemaPending = false;
            } else if (inputPending) {
                inputFile = arg;
                inputPending = false;
            } else if (arg.equals("-output"))
                outputPending = true;
            else if (arg.equals("-workspace"))
                workspacePending = true;
            else if (arg.equals("-schema"))
                schemaPending = true;
            else if (arg.equals("-input"))
                inputPending = true;
            else if (arg.equals("-nocamp"))
                suppressCamp = true;
            else if (arg.startsWith("-"))
                throw new IllegalArgumentException(arg);
            else if (arg.endsWith(".arl") || arg.endsWith(".sem"))
                ordinaryArguments.add(arg);
            else if (arg.contains("::")) {
                ordinaryArguments.add(arg);
            } else
                throw new IllegalArgumentException(arg);
        }

        /* Check restrictions */
        if (ordinaryArguments.isEmpty())
            throw new IllegalArgumentException("No rules specified");

        /* Attempt to load schema, if specified; fail if unable to do so */
        JsonElement schema = schemaFile == null ? null : new JsonParser().parse(new FileReader(schemaFile));

        /* Attempt to load input, if specified; fail if unable to do so */
        JsonObject input = inputFile == null ? null
                : new JsonParser().parse(new FileReader(inputFile)).getAsJsonObject();

        /* Process rule files */
        for (String arg : ordinaryArguments)
            process(arg, outputDir, workspace, schema, input, suppressCamp);
    }

    /**
     * Build an ODMFrontEnd for the designer link case
     * @param workspace the path to the "workspace" (which merely needs to be the parent directory of the project)
     * @param arg the designer link argument
     * @return an ODMFrontEnd containing the deserialized SemRule
     * @throws Exception
     */
    private static ODMFrontEnd getFrontEndForDesignerLink(String workspace, String arg) throws Exception {

        /* Separate parts of name */
        String[] parts = arg.split("::");
        File project = new File(workspace, parts[0]);
        List<String> nameParts = new ArrayList<>(Arrays.asList(parts[1].split("\\.")));
        if (!project.isDirectory() || !new File(project, ".ruleproject").isFile()) {
            throw new IllegalArgumentException(project + " is not a rule project");
        }

        /* Find actual .sem file denoted by the special syntax */
        File dir = new File(project, "output");
        if (!dir.isDirectory()) {
            throw new IllegalArgumentException("The rule project has not been built");
        }
        String simpleName = nameParts.remove(nameParts.size() - 1);
        for (String part : nameParts) {
            dir = new File(dir, part);
            if (!dir.isDirectory()) {
                throw new IllegalArgumentException(
                        "Designer link " + arg + " does not exist or project has not been built");
            }
        }
        File semRuleFile = new File(dir, simpleName + "-brl.sem");
        if (!semRuleFile.isFile()) {
            System.out.println("Designer link " + arg + " does not exist or project has not been built");
            return null;
        }

        /* Deserialize */
        InputStream serialized = new FileInputStream(semRuleFile);
        return new ODMFrontEnd(serialized, null);
    }

    /**
     * Get the result of executing a rule under a RuleEngine.  The mechanisms internal the to the engine may vary
     *   but results come out as console output in the end.
     * @param engine the engine to execute
     * @param wm the input to execute it with
     * @return the output as a String
     */
    private static String getResult(RuleEngine engine, RuleEngineInput wm) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream newSystemOut = new PrintStream(baos);
        PrintStream origSystemOut = System.out;
        System.setOut(newSystemOut);
        try {
            engine.execute(wm);
        } catch (EngineExecutionException | EngineInvalidStateException | IllegalArgumentException e) {
            throw new Error(e);
        }
        newSystemOut.flush();
        System.setOut(origSystemOut);
        return baos.toString();
    }

    /**
     * Initialize the working memory
     * @param engine the engine to initialize
     * @param contents the objects that make up working memory
     * @return the RuleEngineInput to use, including the working memory
     */
    private static RuleEngineInput initWM(RuleEngine engine, List<Object> contents) {
        RuleEngineInput wm = engine.createRuleEngineInput();
        wm.setWorkingMemory(contents);
        return wm;
    }

    /**
     * Format the results of execution as a JSON array
     * @param results the results of execution as a String with one line per element
     */
    private static JsonArray jsonizeOutput(String results) {
        JsonArray ans = new JsonArray();
        for (String line : results.split("[\\n\\r]+")) {
            ans.add(line);
        }
        return ans;
    }

    /**
     * Make a single data object given its type and a JsonObject containing its attributes
     * @param type the type
     * @param attributes the attributes
     * @return the constructed object instance
     * @throws Exception 
     */
    private static Object makeDataObject(String type, JsonObject attributes) throws Exception {
        Object ans = Class.forName(type).newInstance();
        for (Entry<String, JsonElement> attribute : attributes.entrySet()) {
            JsonElement value = attribute.getValue();
            if (value.isJsonPrimitive())
                setAttribute(ans, attribute.getKey(), valueFromJson(value.getAsJsonPrimitive()));
            else if (value.isJsonArray()) {
                JsonArray array = value.getAsJsonArray();
                List<Object> collection = new ArrayList<>();
                setAttribute(ans, attribute.getKey(), collection);
                for (JsonElement element : array) {
                    if (element.isJsonPrimitive())
                        collection.add(valueFromJson((element.getAsJsonPrimitive())));
                    else
                        throw new UnsupportedOperationException(
                                "Only handling collections of primitive type at this time");
                }
            }
        }
        return ans;
    }

    /**
     * Turn an input JSON clause into a list of objects
     * @param input the input clause
     * @return the objects
     * @throws Exception 
     */
    private static List<Object> makeDataObjects(JsonObject input) throws Exception {
        JsonArray items = input.get("WORLD").getAsJsonArray();
        List<Object> result = new ArrayList<>();
        for (JsonElement item : items) {
            JsonObject obj = item.getAsJsonObject();
            JsonArray brands = obj.get("type").getAsJsonArray();
            if (brands.size() != 1)
                throw new UnsupportedOperationException("Don't know how to deal with multiple brands in input");
            String type = brands.get(0).getAsString();
            JsonObject attributes = obj.get("data").getAsJsonObject();
            result.add(makeDataObject(type, attributes));
        }
        return result;
    }

    /**
     * Per-rule subroutine of main program
     * @param arg the validate ordinary argument being processed (will end with .sem or .arl or will contain "::")
     * @param outputDir the specified output directory or null
     * @param workspace the specified workspace or null (will be non-null if arg contains "::")
     * @param schema the schema or null if there is none
     * @param input the input data for ODM engine execution or null if there is none
     * @param suppressCamp true if CAMP output is to be suppressed (only I/O file generation will occur, if that)
     * @throws Exception
     */
    private static void process(String arg, String outputDir, String workspace, JsonElement schema,
            JsonObject input, boolean suppressCamp) throws Exception {

        /* Determine output directory and output file name based on designer link versus file */
        System.out.println("Processing " + arg);
        File dir;
        String ruleName;
        if (arg.contains("::")) {
            dir = outputDir != null ? new File(outputDir) : new File(".");
            ruleName = arg.substring(arg.indexOf("::") + 2);
        } else {
            File source = new File(arg);
            ruleName = source.getName();
            // the following trims any three letter suffix, including .arl and .sem, which at this point, we know are the only possiblities
            ruleName = ruleName.substring(0, ruleName.length() - 4);
            dir = outputDir != null ? new File(outputDir) : source.getParentFile();
        }

        /* Build front end for the three cases (.arl, .sem, designer link) */
        ODMFrontEnd frontEnd;
        if (arg.endsWith(".arl")) {
            if (schema == null)
                System.out.println("No schema provided with ARL file, likely to fail");
            frontEnd = new ODMFrontEnd(arg, schema);
        } else if (arg.endsWith(".sem")) {
            InputStream ser = new FileInputStream(arg);
            frontEnd = new ODMFrontEnd(ser, schema);
        } else {
            frontEnd = getFrontEndForDesignerLink(workspace, arg);
        }

        /* Complete the processing, generating a .camp file */
        if (frontEnd != null && !suppressCamp) {
            SemRule2CAMP semRule2Camp = new SemRule2CAMP(frontEnd.getFactory());
            CampPattern translated = null;
            try {
                translated = semRule2Camp.translate(frontEnd.getRule()).convertToPattern();
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (translated != null)
                Rule2CAMP.writeFile(new File(dir, ruleName.replace(' ', '_') + ".camp"), translated.emit());
        }

        /* If input is provided, execute with ODM engine */
        if (input != null) {
            RuleEngine engine = frontEnd.getEngine();
            List<Object> contents = makeDataObjects(input);
            RuleEngineInput wm = initWM(engine, contents);
            String result = getResult(engine, wm);
            JsonObject combined = new JsonObject();
            combined.add("input", input);
            combined.add("output", jsonizeOutput(result));
            combined.add("schema", schema);
            FileWriter wtr = new FileWriter(new File(dir, ruleName.replace(' ', '_') + ".io"));
            JsonWriter json = new JsonWriter(wtr);
            json.setLenient(true);
            json.setIndent("  ");
            Streams.write(combined, json);
            json.close();
        }
    }

    /**
     * Set an attribute of an Object based on the convention that classes used in this way have public fields for every attribute
     * @param target the Object whose attribute is to be set
     * @param name the name of the attribute
     * @param value the value being set, which should be of the correct type
     */
    private static void setAttribute(Object target, String name, Object value) throws Exception {
        Field field = target.getClass().getField(name);
        field.set(target, value);
    }

    /**
     * Obtain the correct kind of value from a JsonPrimitive node.  The kinds we support are int, String, and boolean
     * @param primitive the JsonPrimitive node
     * @return the value
     */
    private static Object valueFromJson(JsonPrimitive primitive) {
        if (primitive.isString())
            return primitive.getAsString();
        if (primitive.isNumber())
            return primitive.getAsInt();
        if (primitive.isBoolean())
            return primitive.getAsBoolean();
        throw new IllegalStateException();
    }

    /**
     * Write text to a file
     * @param file the file
     * @param text the text to write
     */
    private static void writeFile(File file, String text) throws IOException {
        PrintWriter wtr = new PrintWriter(new FileWriter(file));
        wtr.println(text);
        wtr.close();
    }
}