From bafe762ac01d16904c18404283027e426e19bc73 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Wed, 3 Feb 2021 16:35:48 +0100 Subject: Code reorganization. Moving main code to src, keeping modules in modules/ --- .../main/java/io/trygvis/rules/engine/DbIo.java | 365 +++++++++++++++++++++ .../java/io/trygvis/rules/engine/DbObject.java | 16 + .../main/java/io/trygvis/rules/engine/Engine.java | 172 ++++++++++ .../rules/engine/JinjavaTemplateEngine.java | 51 +++ .../main/java/io/trygvis/rules/engine/Main.java | 21 ++ .../io/trygvis/rules/engine/TemplateLoader.java | 7 + .../trygvis/rules/engine/cli/DatabaseCommand.java | 20 ++ .../io/trygvis/rules/engine/cli/EngineFile.java | 18 + .../io/trygvis/rules/engine/cli/NinjaCommand.java | 156 +++++++++ .../io/trygvis/rules/engine/cli/RunCommand.java | 82 +++++ 10 files changed, 908 insertions(+) create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/DbIo.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/DbObject.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/Engine.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/JinjavaTemplateEngine.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/Main.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/TemplateLoader.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/DatabaseCommand.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/EngineFile.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/NinjaCommand.java create mode 100644 src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/RunCommand.java (limited to 'src/ri-engine/src/main/java/io/trygvis/rules') diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/DbIo.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/DbIo.java new file mode 100644 index 0000000..7dc24ad --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/DbIo.java @@ -0,0 +1,365 @@ +package io.trygvis.rules.engine; + +import ch.qos.logback.core.util.FileUtil; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.introspect.ObjectIdInfo; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import org.drools.core.common.DefaultFactHandle; +import org.drools.core.factmodel.GeneratedFact; +import org.kie.api.KieBase; +import org.kie.api.runtime.KieContainer; +import org.kie.api.runtime.rule.FactHandle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.function.Function; + +@SuppressWarnings("unchecked") +public class DbIo { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ObjectMapper mapper; + + private static final List prioritizedKeys = List.of("key", "name", "fqdn"); + + public DbIo(KieContainer container, KieBase kieBase) { + var factory = new YAMLFactory(); + factory.enable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID); + factory.enable(YAMLGenerator.Feature.USE_NATIVE_OBJECT_ID); + mapper = new ObjectMapper(factory); + mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + var typeFactory = TypeFactory.defaultInstance() + .withClassLoader(new DbClassLoader(container, kieBase)); + mapper.setTypeFactory(typeFactory); + mapper.findAndRegisterModules(); + + mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() { + @Override + public ObjectIdInfo findObjectIdInfo(Annotated a) { + final Class klass = a.getRawType(); + if (GeneratedFact.class.isAssignableFrom(klass)) { + System.out.println("klass = " + klass); + + for (String name : prioritizedKeys) { + try { + final String getter = "get" + name.substring(0, 1).toUpperCase() + name.substring(1); + klass.getMethod(getter); + return new ObjectIdInfo(PropertyName.construct(name), null, ObjectIdGenerators.PropertyGenerator.class, null); + } catch (NoSuchMethodException ignore) { + } + } + System.out.println("a.getRawType() = " + klass); + return new ObjectIdInfo(null, null, ObjectIdGenerators.IntSequenceGenerator.class, null); + } + + return super.findObjectIdInfo(a); + } + }); + } + + public List load(File file) throws IOException { + var parser = mapper.getFactory().createParser(file); + + var objects = parser.>readValueAs(new TypeReference>() {}); + + var items = new ArrayList<>(); + for (DbObject object : objects) { + try { + var type = mapper.getTypeFactory().findClass(object.type); + var x = mapper.treeToValue(object.data, type); + if (x == null) { + x = type.getDeclaredConstructor().newInstance(); + } + items.add(x); + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + System.out.println("e.getClass() = " + e.getClass().getName()); + System.out.println("e.getMessage() = " + e.getMessage()); + // ignore + } + } + + return items; + } + + public void dump(File file, Collection factHandles) throws IOException { + dump(file, factHandles, (o) -> true); + } + + // This should just sort by all getters instead. + static class FactCollection { + public final Class type; + public final List values; + + public FactCollection(Class type) { + this.type = type; + this.values = new ArrayList<>(); + } + + public void sort() { + var comparator = comparable(type); + + this.values.sort(comparator); + } + } + + private static final Map, Comparator> comparators = new HashMap<>(); + + private static > Comparator comparable(Class klass) { + var comparator = comparators.get(klass); + if (comparator != null) { + return comparator; + } + + // TODO: check if klass is a Comparable directly. + + var discoveredFieldsP1 = new LinkedHashMap>(); + var discoveredFieldsP2 = new LinkedHashMap>(); + + var prioritizedTypes = List.of(String.class, int.class, Number.class); + + for (var f : klass.getDeclaredFields()) { + if (f.getDeclaringClass() == Object.class) { + continue; + } + + if (!f.trySetAccessible()) { + continue; + } + + var collection = discoveredFieldsP2; + + if (prioritizedTypes.contains(f.getType())) { + collection = discoveredFieldsP1; + } + + collection.put(f.getName(), (Object o) -> { + try { + return f.get(o); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + } + +// for (var m : klass.getFields()) { +// if (m.getParameterCount() != 0) { +// continue; +// } +// +// var name = m.getName(); +// +// if (name.startsWith("get") && name.length() > 3 && Character.isUpperCase(name.charAt(4))) { +// name = name.substring(3, 3).toLowerCase() + name.substring(4); +// } else { +// continue; +// } +// +// if (!m.isAccessible()) { +// if (!m.trySetAccessible()) +// return null; +// } +// +// discoveredFields.put(name, m); +// } + +// System.out.printf("Sorting %s by:%n", klass.getName()); + + var discoveredFields = new LinkedHashMap<>(discoveredFieldsP1); + discoveredFields.putAll(discoveredFieldsP2); + + List> accessors = new ArrayList<>(); + for (String prioritizedKey : prioritizedKeys) { + var m = discoveredFields.remove(prioritizedKey); + if (m == null) { + continue; + } + + accessors.add(m); +// System.out.println(" + " + prioritizedKey); + } + accessors.addAll(discoveredFields.values()); +// discoveredFields.keySet().forEach((s)-> System.out.println(" - " + s)); + + comparator = (a, b) -> { +// if (klass.getName().contains("AcmeServer")) { +// System.out.println("AcmeIo.comparable"); +// } + + for (var method : accessors) { + var x = method.apply(a); + var y = method.apply(b); + + if (x == null && y == null) { + continue; + } + + if (x == null) { + return -1; + } else if (y == null) { + return 1; + } else { + var res = x.toString().compareTo(y.toString()); + if (res != 0) { + return res; + } + } + } + + return 0; + }; + + comparators.put(klass, comparator); + + return comparator; + } + + static record DbObject2(String type, Object data) { + } + + public void dump(File file, Collection factHandles, Function filter) throws IOException { + FileUtil.createMissingParentDirectories(file); + + var facts = new TreeMap, FactCollection>(Comparator.comparing(Class::getName)); + for (var handle : factHandles) { + if (handle instanceof DefaultFactHandle h) { + var obj = h.getObject(); + if (!filter.apply(obj)) { + continue; + } + + Class type = obj.getClass(); + var collection = facts.get(type); + + if (collection == null) { + collection = new FactCollection(type); + facts.put(type, collection); + } + + collection.values.add(obj); + } + } + + var objects = new ArrayList(facts.size()); + for (var e : facts.entrySet()) { + var name = e.getKey().getName(); + + var collection = e.getValue(); + collection.sort(); + for (var fact : collection.values) { + objects.add(new DbObject2(name, fact)); + } + } + + objects.sort(new DbObjectComparator()); + + var factory = mapper.getFactory(); + try (var writer = new FileWriter(file); + var g = factory.createGenerator(writer)) { + g.writeObject(objects); + } + } + + private class DbClassLoader extends ClassLoader { + private final KieContainer container; + private final KieBase kieBase; + + public DbClassLoader(KieContainer container, KieBase kieBase) { + this.container = container; + this.kieBase = kieBase; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + logger.info("Loading class {}", name); + try { + var klass = super.loadClass(name); + logger.info("Found class in super classloader"); + return klass; + } catch (ClassNotFoundException e) { + var i = name.lastIndexOf('.'); + String pkg, simpleName; + if (i == -1) { + pkg = null; + simpleName = name; + } else { + pkg = name.substring(0, i); + simpleName = name.substring(i + 1); + } + + try { + var klass = container.getClassLoader().loadClass(name); + logger.info("Found class in container's classloader"); + return klass; + } catch (ClassNotFoundException ignore) { + } + + try { + logger.info("pkg = {}", pkg); + logger.info("simpleName = {}", simpleName); + var clazz = kieBase.getFactType(pkg, simpleName); + if (clazz != null) { + logger.info("Found class as a FactType"); + return clazz.getFactClass(); + } + } catch (UnsupportedOperationException ignore) { + } + + logger.warn("Class not found: {}", name); + + throw e; + } + } + } + + private static class DbObjectComparator implements Comparator { + private final List prioritizedPackages = List.of( + "io.trygvis.rules.machine", + "io.trygvis.rules.network", + "io.trygvis.rules.dns", + "io.trygvis.rules.dba", + "io.trygvis.rules", + "io.trygvis.rules.core"); + + @Override + public int compare(DbObject2 a, DbObject2 b) { + var indexA = a.type.lastIndexOf("."); + String packageA = indexA == -1 ? null : a.type.substring(0, indexA); + String classA = indexA == -1 ? a.type : a.type.substring(indexA + 1); + + var indexB = b.type.lastIndexOf("."); + String packageB = indexB == -1 ? null : b.type.substring(0, indexB); + String classB = indexB == -1 ? b.type : b.type.substring(indexB + 1); + + var priIdxA = prioritizedPackages.indexOf(packageA); + var priIdxB = prioritizedPackages.indexOf(packageB); + + if (priIdxA == -1 && priIdxB == -1) { + return classB.compareTo(classA); + } else if (priIdxA == -1) { + return 1; + } else if (priIdxB == -1) { + return -1; + } + return priIdxA - priIdxB; +// var diff = priIdxB - priIdxA; +// if (diff != 0) { +// return diff; +// } +// +// return classB.compareTo(classA); + } + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/DbObject.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/DbObject.java new file mode 100644 index 0000000..6b9817e --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/DbObject.java @@ -0,0 +1,16 @@ +package io.trygvis.rules.engine; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public final class DbObject { + public String type; + public ObjectNode data; + + public DbObject() { + } + + public DbObject(String type, ObjectNode data) { + this.type = type; + this.data = data; + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/Engine.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/Engine.java new file mode 100644 index 0000000..f2247d3 --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/Engine.java @@ -0,0 +1,172 @@ +package io.trygvis.rules.engine; + +import org.drools.core.audit.WorkingMemoryConsoleLogger; +import org.drools.core.base.MapGlobalResolver; +import org.drools.reflective.classloader.ProjectClassLoader; +import org.kie.api.KieServices; +import org.kie.api.event.rule.AgendaEventListener; +import org.kie.api.event.rule.RuleRuntimeEventListener; +import org.kie.api.io.Resource; +import org.kie.api.runtime.KieContainer; +import org.kie.api.runtime.KieSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class Engine implements Closeable { + @SuppressWarnings("FieldCanBeLocal") + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public final String name; + @Nullable + public final File output; + public final DbIo io; + public final KieSession session; + + public Engine(String name, File[] databases, @Nullable File output, String[] agendaGroups, File[] modules) + throws IOException { + this.name = name; + this.output = output; + + logger.info("Getting KieServices"); + + var services = KieServices.Factory.get(); + + var kieRepository = services.getRepository(); + + KieContainer container; + TemplateLoader templateLoader; + if (modules != null && modules.length > 0) { + List resources = new ArrayList<>(); + List files = new ArrayList<>(); + for (File path : modules) { + if (!path.exists()) { + logger.warn("Module path does not exist: {}", path.getAbsolutePath()); + continue; + } + + logger.info("New KieBuilder: {}, file={}, directory={}", path, path.isFile(), path.isDirectory()); + + if (path.isFile()) { + files.add(path.toURI().toURL()); + } + + var resource = services.getResources().newFileSystemResource(path); + logger.info("resource.getResourceType() = {}", resource.getResourceType()); + resources.add(resource); + } + + var module = kieRepository.addKieModule(resources.get(0), resources.subList(1, resources.size()).toArray(new Resource[0])); + logger.info("module.getReleaseId() = {}", module.getReleaseId()); + var rId = module.getReleaseId(); + + logger.info("Creating classpath container, releaseId=" + rId); + container = services.newKieContainer(rId); + + templateLoader = new ClasspathTemplateLoader(new URLClassLoader(files.toArray(new URL[0]))); + } else { + var classLoader = ProjectClassLoader.findParentClassLoader(); + container = services.getKieClasspathContainer(classLoader); + templateLoader = new ClasspathTemplateLoader(classLoader); + } + + logger.info("Creating KieBase \"{}\"", name); + logger.info("Available kie base names: {}", container.getKieBaseNames()); + var kieBase = container.getKieBase(name); + + session = container.newKieSession(name); + + var l = new WorkingMemoryConsoleLogger(session); + session.addEventListener((AgendaEventListener) l); + session.addEventListener((RuleRuntimeEventListener) l); + + session.getGlobals().setDelegate(new EngineGlobalResolver(templateLoader)); + + logger.info("Loading data"); + io = new DbIo(container, kieBase); + + List allObjects = new ArrayList<>(); + for (File database : databases) { + var objects = io.load(database); + + if (objects.isEmpty()) { + logger.warn("Did not load any objects, something is wrong"); + return; + } + + logger.info("Loaded {} objects from {}", objects.size(), database); + allObjects.addAll(objects); + } + logger.info("Loaded {} objects", allObjects.size()); + + for (var object : allObjects) { + logger.info("object = " + object); + session.insert(object); + } + + for (var agendaGroup : agendaGroups) { + logger.info("Setting agenda: " + agendaGroup); + session.getAgenda().getAgendaGroup(agendaGroup).setFocus(); + session.fireAllRules(); + } + } + + @Override + public void close() { + session.dispose(); + } + + private static class ClasspathTemplateLoader implements TemplateLoader { + private final ClassLoader classLoader; + + private ClasspathTemplateLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public String load(String name) throws IOException { + var resource = "templates/" + name + ".j2"; + + try (var inputStream = classLoader.getResourceAsStream(resource)) { + if (inputStream == null) { + throw new FileNotFoundException("Classpath resource: " + resource); + } + + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } + } + + private class EngineGlobalResolver extends MapGlobalResolver { + private final TemplateLoader templateLoader; + + public EngineGlobalResolver() { + templateLoader = null; + } + + public EngineGlobalResolver(TemplateLoader templateLoader) { + this.templateLoader = templateLoader; + } + + @Override + public Object resolveGlobal(String identifier) { + if ("te".equals(identifier)) { + if (output == null) { + throw new IllegalArgumentException("An instance of the TemplateEngine is required, but this job is not configured with a output directory."); + } + return new JinjavaTemplateEngine(templateLoader, output); + } + return super.resolveGlobal(identifier); + } + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/JinjavaTemplateEngine.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/JinjavaTemplateEngine.java new file mode 100644 index 0000000..ba23089 --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/JinjavaTemplateEngine.java @@ -0,0 +1,51 @@ +package io.trygvis.rules.engine; + +import ch.qos.logback.core.util.FileUtil; +import com.hubspot.jinjava.Jinjava; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Map; +import java.util.Objects; + +/** + * TODO: cache templates. + */ +public class JinjavaTemplateEngine implements TemplateEngine { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Jinjava jinjava = new Jinjava(); + + private final TemplateLoader loader; + private final File basedir; + + public JinjavaTemplateEngine(TemplateLoader templateLoader, File basedir) { + Objects.requireNonNull(templateLoader); + Objects.requireNonNull(basedir); + this.loader = templateLoader; + this.basedir = basedir; + } + + @Override + public void clean() { + try { + logger.info("Removing output directory: {}", basedir); + FileUtils.deleteDirectory(basedir); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void template(String name, String output, Map params) throws IOException { + var template = loader.load(name); + String renderedTemplate = jinjava.render(template, params); + var f = new File(basedir, output); + FileUtil.createMissingParentDirectories(f); + Files.writeString(f.toPath(), renderedTemplate); + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/Main.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/Main.java new file mode 100644 index 0000000..dc9ed02 --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/Main.java @@ -0,0 +1,21 @@ +package io.trygvis.rules.engine; + +import io.trygvis.rules.engine.cli.DatabaseCommand; +import io.trygvis.rules.engine.cli.NinjaCommand; +import io.trygvis.rules.engine.cli.RunCommand; +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command( + name = "engine", + subcommands = {RunCommand.class, DatabaseCommand.class, NinjaCommand.class}, + mixinStandardHelpOptions = true, + version = "UNSPECIFIED") +class Main { + + public static void main(String... args) { + //noinspection InstantiationOfUtilityClass + int exitCode = new CommandLine(new Main()).execute(args); + System.exit(exitCode); + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/TemplateLoader.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/TemplateLoader.java new file mode 100644 index 0000000..9441264 --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/TemplateLoader.java @@ -0,0 +1,7 @@ +package io.trygvis.rules.engine; + +import java.io.IOException; + +interface TemplateLoader { + String load(String name) throws IOException; +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/DatabaseCommand.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/DatabaseCommand.java new file mode 100644 index 0000000..03650b5 --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/DatabaseCommand.java @@ -0,0 +1,20 @@ +package io.trygvis.rules.engine.cli; + +import picocli.CommandLine.Command; + +import java.util.concurrent.Callable; + +import static picocli.CommandLine.Option; + +@Command(name = "database") +public class DatabaseCommand implements Callable { + + @Option(names = {"-v", "--verbose"}) + private boolean verbose; + + @Override + public Integer call() { + System.out.println("DatabaseCommand.call"); + return 0; + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/EngineFile.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/EngineFile.java new file mode 100644 index 0000000..8da0e39 --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/EngineFile.java @@ -0,0 +1,18 @@ +package io.trygvis.rules.engine.cli; + +import java.util.ArrayList; +import java.util.List; + +public class EngineFile { + public String dbDir; + public List jobs; + + public static class Job { + public String name; + public List inputs = new ArrayList<>(); + public List outputIncludes = new ArrayList<>(); + public String generatedOutput; + public List agendaGroups = new ArrayList<>(); + public List modules = new ArrayList<>(); + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/NinjaCommand.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/NinjaCommand.java new file mode 100644 index 0000000..ef5ed1d --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/NinjaCommand.java @@ -0,0 +1,156 @@ +package io.trygvis.rules.engine.cli; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import static picocli.CommandLine.Command; + +@Command(name = "ninja") +public class NinjaCommand implements Callable { + + public File basedir = null; + + public Path basepath; + + @Override + public Integer call() throws Exception { + basepath = Objects.requireNonNullElseGet(basedir, () -> new File("").getAbsoluteFile()).toPath(); + + var factory = new YAMLFactory(); + factory.enable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID); + factory.enable(YAMLGenerator.Feature.USE_NATIVE_OBJECT_ID); + var mapper = new ObjectMapper(factory); + mapper.enable(MapperFeature.AUTO_DETECT_FIELDS); + + var f = mapper.readValue(new File(basedir, "engine.yaml"), EngineFile.class); + + Path dbDir; + if (StringUtils.trimToNull(f.dbDir) == null) { + System.err.println("Missing required field: dbDir"); + return 1; + } else { + dbDir = Path.of(f.dbDir); + } + + var buf = new StringWriter(); + var out = new PrintWriter(buf); + + out.println("# Generated"); + out.println("#"); + out.println("### engine.ninja"); + out.println(""); + out.println("rule engine-yaml-to-ninja"); + out.println(" command = engine ninja"); + out.println(""); + out.println("build engine.ninja: engine-yaml-to-ninja engine.yaml"); + out.println(""); + out.println("### engine.png"); + out.println(""); + out.println("rule ninja-to-dot"); + out.println(" command = ninja -t graph > $out"); + out.println(""); + out.println("rule dot-to-png"); + out.println(" command = dot -Tpng < $in > $out"); + out.println(""); + out.println("build engine.dot: ninja-to-dot build.ninja engine.ninja"); + out.println(""); + out.println("build engine.png: dot-to-png engine.dot"); + out.println(""); + out.println("# Jobs"); + + for (var job : f.jobs) { + out.println(""); + + out.println("rule %s".formatted(job.name)); + out.println(" command=engine run $name $inputs $output_state $output_includes $generated_output $agenda_groups $modules"); + out.println(); + var dependencies = job.inputs.stream() + .map(s -> dbDir.resolve(s + ".yaml").toString()) + .collect(Collectors.joining(" ")); + + var outputState = dbDir.resolve(job.name + ".yaml"); + + var generated = List.of(outputState).stream() + .map(Path::toString) + .collect(Collectors.joining(" ")); + + out.println("build %s: %s %s".formatted(generated, job.name, dependencies)); + out.println(" name=--name %s".formatted(job.name)); + + if (!job.inputs.isEmpty()) { + var is = job.inputs.stream() + .map(s -> "--input=" + dbDir.resolve(s + ".yaml")) + .collect(Collectors.joining(" $\n ", "\n ", "")); + + out.println(" inputs=$%s".formatted(is)); + } + + out.println(" output_state=--output-state %s".formatted(outputState)); + if (!job.outputIncludes.isEmpty()) { + var str = job.outputIncludes.stream() + .map(s -> "--output-include=" + s) + .collect(Collectors.joining(" $\n ", "\n ", "")); + + out.println(" output_includes=$%s".formatted(str)); + } + + if (job.generatedOutput != null) { + out.println(" generated_output=--generated-output %s".formatted(fixPath(job.generatedOutput))); + } + + if (!job.agendaGroups.isEmpty()) { + var ag = job.agendaGroups.stream() + .map(s -> "--agenda-group=" + s) + .collect(Collectors.joining(" $\n ", "\n ", "")); + out.println(" agenda_groups=%s".formatted(ag)); + } + + if (!job.modules.isEmpty()) { + var ms = job.modules.stream() + .map(this::fixPath) + .map(s -> "--module=" + s) + .collect(Collectors.joining(" $\n ", "\n ", "")); + + out.println(" modules=$%s".formatted(ms)); + } + } + + var ninjaFile = new File(basedir, "engine.ninja"); + try (var writer = new FileWriter(ninjaFile)) { + writer.write(buf.toString()); + } + + return 0; + } + + private String fixPath(String s) { + if (s.startsWith("$MODULE_HOME/")) { + s = "$" + s; + } + + var p = Path.of(s); + + if (p.isAbsolute()) { + s = basepath.relativize(Path.of(s)).toString(); + } + + if (s.contains("*")) { + s = "$$(echo " + s + ")"; + } + + return s; + } +} diff --git a/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/RunCommand.java b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/RunCommand.java new file mode 100644 index 0000000..35f30cd --- /dev/null +++ b/src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/RunCommand.java @@ -0,0 +1,82 @@ +package io.trygvis.rules.engine.cli; + +import io.trygvis.rules.engine.Engine; + +import java.io.File; +import java.util.concurrent.Callable; + +import static picocli.CommandLine.Command; +import static picocli.CommandLine.Option; + +@Command(name = "run") +public class RunCommand implements Callable { + + @Option(names = {"-n", "--name"}) + public String name; + + @Option(names = {"-i", "--input"}) + public File[] input; + + @Option(names = {"--output-state"}) + public File outputState; + + @Option(names = {"--output-include"}, split = ",", arity = "1..*") + public String[] outputIncludes; + + @Option(names = {"--generated-output"}) + public File generatedOutput; + + @Option(names = {"--agenda-group"}) + public String[] agendaGroups; + + @Option(names = {"--module"}, split = ",", arity = "1..*") + public File[] module; + + @Override + public Integer call() throws Exception { + + if (agendaGroups == null || agendaGroups.length == 0) { + agendaGroups = new String[]{"init", "generate"}; + } + + try (var engine = new Engine(name, input, generatedOutput, agendaGroups, module)) { + engine.io.dump(outputState, engine.session.getFactHandles(), (Object o) -> + { + if (outputIncludes == null || outputIncludes.length == 0) { + return true; + } + + var name = o.getClass().getName(); + var simpleName = o.getClass().getSimpleName(); + + for (var i : outputIncludes) { + var ok = false; + if (i.startsWith("*")) { + i = i.substring(1); + + if (i.endsWith("*")) { + i = i.substring(1, i.length() - 2); + ok = name.contains(i); + } else { + ok = name.startsWith(i) || simpleName.startsWith(i); + } + } else if (i.endsWith("*")) { + i = i.substring(0, i.length() - 2); + ok = name.startsWith(i) || simpleName.startsWith(i); + } else { + ok = name.equals(i) || simpleName.equals(i); + } + + if (ok) { + return true; + } + } + + return false; + } + ); + } + + return 0; + } +} -- cgit v1.2.3