Java tutorial
/** * (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(); } }