diff options
Diffstat (limited to 'src')
28 files changed, 1715 insertions, 0 deletions
diff --git a/src/ri-engine/classpath.txt b/src/ri-engine/classpath.txt new file mode 100644 index 0000000..f01f5f8 --- /dev/null +++ b/src/ri-engine/classpath.txt @@ -0,0 +1,62 @@ +io.trygvis.rules-sandbox:ri-engine:1.0-SNAPSHOT:jar +ch.obermuhlner:big-math:2.0.0:jar +ch.qos.logback:logback-classic:1.2.3:jar +ch.qos.logback:logback-core:1.2.3:jar +com.fasterxml.jackson.core:jackson-annotations:2.12.0:jar +com.fasterxml.jackson.core:jackson-core:2.12.0:jar +com.fasterxml.jackson.core:jackson-databind:2.12.0:jar +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.0:jar +com.github.javaparser:javaparser-core:3.13.10:jar +com.github.virtuald:curvesapi:1.06:jar +com.google.code.findbugs:annotations:3.0.1:jar +com.google.errorprone:error_prone_annotations:2.1.3:jar +com.google.guava:guava:25.0-jre:jar +com.google.j2objc:j2objc-annotations:1.1:jar +com.google.re2j:re2j:1.2:jar +com.googlecode.java-ipv6:java-ipv6:0.17:jar +com.hubspot.jinjava:jinjava:2.5.6:jar +com.thoughtworks.xstream:xstream:1.4.14:jar +com.zaxxer:SparseBitSet:1.2:jar +commons-codec:commons-codec:1.14:jar +commons-io:commons-io:2.8.0:jar +commons-net:commons-net:3.3:jar +info.picocli:picocli:4.6.1:jar +io.trygvis.rules-sandbox.module:ri-module-api:1.0-SNAPSHOT:jar +org.antlr:antlr-runtime:3.5.2:jar +org.apache.commons:commons-collections4:4.4:jar +org.apache.commons:commons-compress:1.19:jar +org.apache.commons:commons-lang3:3.10:jar +org.apache.commons:commons-math3:3.6.1:jar +org.apache.poi:poi:4.1.2:jar +org.apache.poi:poi-ooxml:4.1.2:jar +org.apache.poi:poi-ooxml-schemas:4.1.2:jar +org.apache.xmlbeans:xmlbeans:3.1.0:jar +org.checkerframework:checker-compat-qual:2.0.0:jar +org.codehaus.mojo:animal-sniffer-annotations:1.14:jar +org.drools:drools-canonical-model:7.48.0.Final:jar +org.drools:drools-compiler:7.48.0.Final:jar +org.drools:drools-core:7.48.0.Final:jar +org.drools:drools-core-dynamic:7.48.0.Final:jar +org.drools:drools-core-reflective:7.48.0.Final:jar +org.drools:drools-decisiontables:7.48.0.Final:jar +org.drools:drools-ecj:7.48.0.Final:jar +org.drools:drools-model-compiler:7.48.0.Final:jar +org.drools:drools-mvel:7.48.0.Final:jar +org.drools:drools-mvel-compiler:7.48.0.Final:jar +org.drools:drools-mvel-parser:7.48.0.Final:jar +org.drools:drools-templates:7.48.0.Final:jar +org.javassist:javassist:3.24.1-GA:jar +org.jsoup:jsoup:1.10.3:jar +org.kie:kie-api:7.48.0.Final:jar +org.kie:kie-internal:7.48.0.Final:jar +org.kie:kie-memory-compiler:7.48.0.Final:jar +org.kie.soup:kie-soup-commons:7.48.0.Final:jar +org.kie.soup:kie-soup-maven-support:7.48.0.Final:jar +org.kie.soup:kie-soup-project-datamodel-api:7.48.0.Final:jar +org.kie.soup:kie-soup-project-datamodel-commons:7.48.0.Final:jar +org.kie.soup:kie-soup-xstream:7.48.0.Final:jar +org.mvel:mvel2:2.4.11.Final:jar +org.slf4j:slf4j-api:1.7.30:jar +org.yaml:snakeyaml:1.26:jar +xmlpull:xmlpull:1.1.3.1:jar +xpp3:xpp3_min:1.1.4c:jar diff --git a/src/ri-engine/pom.xml b/src/ri-engine/pom.xml new file mode 100644 index 0000000..e974b7c --- /dev/null +++ b/src/ri-engine/pom.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <properties> + <main-class>io.trygvis.rules.engine.Main</main-class> + </properties> + + <parent> + <groupId>io.trygvis.rules-sandbox</groupId> + <artifactId>rules-sandbox</artifactId> + <version>1.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>ri-engine</artifactId> + + <dependencies> + + <dependency> + <groupId>${project.groupId}.module</groupId> + <artifactId>ri-module-api</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.drools</groupId> + <artifactId>drools-core</artifactId> + </dependency> + <dependency> + <groupId>org.drools</groupId> + <artifactId>drools-compiler</artifactId> + </dependency> + <dependency> + <groupId>org.drools</groupId> + <artifactId>drools-decisiontables</artifactId> + </dependency> + <dependency> + <groupId>org.drools</groupId> + <artifactId>drools-templates</artifactId> + </dependency> + + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-yaml</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + </dependency> + + <dependency> + <groupId>com.hubspot.jinjava</groupId> + <artifactId>jinjava</artifactId> + <version>2.5.6</version> + </dependency> + + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + <dependency> + <groupId>info.picocli</groupId> + <artifactId>picocli</artifactId> + <version>${version.picocli}</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <annotationProcessorPaths> + <path> + <groupId>info.picocli</groupId> + <artifactId>picocli-codegen</artifactId> + <version>${version.picocli}</version> + </path> + </annotationProcessorPaths> + <compilerArgs combine.children="append"> + <arg>-Aproject=${project.groupId}/${project.artifactId}</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>3.2.4</version> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <transformers> + <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> + <resource>META-INF/kie.conf</resource> + </transformer> + </transformers> + <outputFile>${project.build.directory}/${project.artifactId}-fat.jar</outputFile> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.2.0</version> + <configuration> + <archive> + <manifest> + <mainClass>${main-class}</mainClass> + </manifest> + </archive> + </configuration> + </plugin> + + <!-- TODO: attach generated script to build --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.6.0</version> + <executions> + <execution> + <id>generate-autocompletion-script</id> + <phase>package</phase> + <goals> + <!-- The java goal doesn't work here as maven itself would have to run with enable-preview --> + <goal>exec</goal> + </goals> + </execution> + </executions> + <configuration> + <executable>java</executable> + <arguments> + <argument>-Dpicocli.autocomplete.systemExitOnError</argument> + <argument>${java.preview}</argument> + <argument>-cp</argument> + <classpath/> + <argument>picocli.AutoComplete</argument> + <argument>--force</argument> + <argument>--completionScript</argument> + <argument>${project.build.directory}/engine_completion.sh</argument> + <argument>${main-class}</argument> + </arguments> + </configuration> + </plugin> + </plugins> + </build> +</project> 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<String> 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<Object> load(File file) throws IOException { + var parser = mapper.getFactory().createParser(file); + + var objects = parser.<List<DbObject>>readValueAs(new TypeReference<List<DbObject>>() {}); + + 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<FactHandle> factHandles) throws IOException { + dump(file, factHandles, (o) -> true); + } + + // This should just sort by all getters instead. + static class FactCollection<T> { + public final Class<T> type; + public final List<T> values; + + public FactCollection(Class<T> type) { + this.type = type; + this.values = new ArrayList<>(); + } + + public void sort() { + var comparator = comparable(type); + + this.values.sort(comparator); + } + } + + private static final Map<Class<?>, Comparator> comparators = new HashMap<>(); + + private static <A, T extends Comparable<T>> Comparator comparable(Class<A> klass) { + var comparator = comparators.get(klass); + if (comparator != null) { + return comparator; + } + + // TODO: check if klass is a Comparable directly. + + var discoveredFieldsP1 = new LinkedHashMap<String, Function<Object, Object>>(); + var discoveredFieldsP2 = new LinkedHashMap<String, Function<Object, Object>>(); + + 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<Function<Object, Object>> 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<FactHandle> factHandles, Function<Object, Boolean> filter) throws IOException { + FileUtil.createMissingParentDirectories(file); + + var facts = new TreeMap<Class<?>, FactCollection<Object>>(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<DbObject2>(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<DbObject2> { + private final List<String> 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<Resource> resources = new ArrayList<>(); + List<URL> 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<Object> 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<String, Object> 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<Integer> { + + @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<Job> jobs; + + public static class Job { + public String name; + public List<String> inputs = new ArrayList<>(); + public List<String> outputIncludes = new ArrayList<>(); + public String generatedOutput; + public List<String> agendaGroups = new ArrayList<>(); + public List<String> 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<Integer> { + + 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<Integer> { + + @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; + } +} diff --git a/src/ri-it/classpath.txt b/src/ri-it/classpath.txt new file mode 100644 index 0000000..0b29855 --- /dev/null +++ b/src/ri-it/classpath.txt @@ -0,0 +1,63 @@ +io.trygvis.rules-sandbox:ri-it:1.0-SNAPSHOT:jar +ch.obermuhlner:big-math:2.0.0:jar +ch.qos.logback:logback-classic:1.2.3:jar +ch.qos.logback:logback-core:1.2.3:jar +com.fasterxml.jackson.core:jackson-annotations:2.12.0:jar +com.fasterxml.jackson.core:jackson-core:2.12.0:jar +com.fasterxml.jackson.core:jackson-databind:2.12.0:jar +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.0:jar +com.github.javaparser:javaparser-core:3.13.10:jar +com.github.virtuald:curvesapi:1.06:jar +com.google.code.findbugs:annotations:3.0.1:jar +com.google.errorprone:error_prone_annotations:2.1.3:jar +com.google.guava:guava:25.0-jre:jar +com.google.j2objc:j2objc-annotations:1.1:jar +com.google.re2j:re2j:1.2:jar +com.googlecode.java-ipv6:java-ipv6:0.17:jar +com.hubspot.jinjava:jinjava:2.5.6:jar +com.thoughtworks.xstream:xstream:1.4.14:jar +com.zaxxer:SparseBitSet:1.2:jar +commons-codec:commons-codec:1.14:jar +commons-io:commons-io:2.8.0:jar +commons-net:commons-net:3.3:jar +info.picocli:picocli:4.6.1:jar +io.trygvis.rules-sandbox:ri-engine:1.0-SNAPSHOT:jar +io.trygvis.rules-sandbox.module:ri-module-api:1.0-SNAPSHOT:jar +org.antlr:antlr-runtime:3.5.2:jar +org.apache.commons:commons-collections4:4.4:jar +org.apache.commons:commons-compress:1.19:jar +org.apache.commons:commons-lang3:3.10:jar +org.apache.commons:commons-math3:3.6.1:jar +org.apache.poi:poi:4.1.2:jar +org.apache.poi:poi-ooxml:4.1.2:jar +org.apache.poi:poi-ooxml-schemas:4.1.2:jar +org.apache.xmlbeans:xmlbeans:3.1.0:jar +org.checkerframework:checker-compat-qual:2.0.0:jar +org.codehaus.mojo:animal-sniffer-annotations:1.14:jar +org.drools:drools-canonical-model:7.48.0.Final:jar +org.drools:drools-compiler:7.48.0.Final:jar +org.drools:drools-core:7.48.0.Final:jar +org.drools:drools-core-dynamic:7.48.0.Final:jar +org.drools:drools-core-reflective:7.48.0.Final:jar +org.drools:drools-decisiontables:7.48.0.Final:jar +org.drools:drools-ecj:7.48.0.Final:jar +org.drools:drools-model-compiler:7.48.0.Final:jar +org.drools:drools-mvel:7.48.0.Final:jar +org.drools:drools-mvel-compiler:7.48.0.Final:jar +org.drools:drools-mvel-parser:7.48.0.Final:jar +org.drools:drools-templates:7.48.0.Final:jar +org.javassist:javassist:3.24.1-GA:jar +org.jsoup:jsoup:1.10.3:jar +org.kie:kie-api:7.48.0.Final:jar +org.kie:kie-internal:7.48.0.Final:jar +org.kie:kie-memory-compiler:7.48.0.Final:jar +org.kie.soup:kie-soup-commons:7.48.0.Final:jar +org.kie.soup:kie-soup-maven-support:7.48.0.Final:jar +org.kie.soup:kie-soup-project-datamodel-api:7.48.0.Final:jar +org.kie.soup:kie-soup-project-datamodel-commons:7.48.0.Final:jar +org.kie.soup:kie-soup-xstream:7.48.0.Final:jar +org.mvel:mvel2:2.4.11.Final:jar +org.slf4j:slf4j-api:1.7.30:jar +org.yaml:snakeyaml:1.26:jar +xmlpull:xmlpull:1.1.3.1:jar +xpp3:xpp3_min:1.1.4c:jar diff --git a/src/ri-it/pom.xml b/src/ri-it/pom.xml new file mode 100644 index 0000000..6ccefad --- /dev/null +++ b/src/ri-it/pom.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>io.trygvis.rules-sandbox</groupId> + <artifactId>rules-sandbox</artifactId> + <version>1.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>ri-it</artifactId> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>ri-engine</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>io.trygvis.rules-sandbox.module</groupId> + <artifactId>ri-base</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.trygvis.rules-sandbox.module</groupId> + <artifactId>ri-wireguard</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.trygvis.rules-sandbox.module</groupId> + <artifactId>acme</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeAppsTestMain.java b/src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeAppsTestMain.java new file mode 100644 index 0000000..a512daf --- /dev/null +++ b/src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeAppsTestMain.java @@ -0,0 +1,20 @@ +package io.trygvis.rules.engine; + +import io.trygvis.rules.engine.cli.RunCommand; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AcmeAppsTestMain { + public static void main(String[] args) throws Exception { + var basedir = new File("example"); + var c = new RunCommand(); + c.name = "acme-apps"; + c.input = new File[]{new File(basedir, "acme.yaml")}; + c.outputState = new File(basedir, "db/apps.yaml"); + c.agendaGroups = new String[]{"init", "generate"}; + c.generatedOutput = new File(basedir, "acme-apps"); + assertEquals(0, c.call()); + } +} diff --git a/src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeWireguardTestMain.java b/src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeWireguardTestMain.java new file mode 100644 index 0000000..243b07d --- /dev/null +++ b/src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeWireguardTestMain.java @@ -0,0 +1,28 @@ +package io.trygvis.rules.engine; + +import io.trygvis.rules.engine.cli.RunCommand; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AcmeWireguardTestMain { + public static void main(String[] args) throws Exception { + var basedir = new File("example"); + var c = new RunCommand(); + c.name = "acme-wireguard"; + c.input = new File[]{new File(basedir, "acme.yaml")}; + c.outputState = new File(basedir, "db/wireguard.yaml"); + c.agendaGroups = new String[]{"init", "generate"}; + c.generatedOutput = new File(basedir, "acme-wireguard"); + c.outputIncludes = new String[]{ + "Wg*", + "Machine", + "DnsEntry", + "Ipv4Cidr", + "Ipv4Address", + }; + + assertEquals(0, c.call()); + } +} diff --git a/src/ri-it/src/test/java/io/trygvis/rules/engine/NinjaTestMain.java b/src/ri-it/src/test/java/io/trygvis/rules/engine/NinjaTestMain.java new file mode 100644 index 0000000..d62eec1 --- /dev/null +++ b/src/ri-it/src/test/java/io/trygvis/rules/engine/NinjaTestMain.java @@ -0,0 +1,16 @@ +package io.trygvis.rules.engine; + +import io.trygvis.rules.engine.cli.NinjaCommand; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NinjaTestMain { + public static void main(String[] args) throws Exception { + var c = new NinjaCommand(); + c.basedir = new File("example"); + + assertEquals(0, c.call()); + } +} diff --git a/src/ri-planner/classpath.txt b/src/ri-planner/classpath.txt new file mode 100644 index 0000000..411ee43 --- /dev/null +++ b/src/ri-planner/classpath.txt @@ -0,0 +1,90 @@ +io.trygvis.rules-sandbox:acme-planner:1.0-SNAPSHOT:jar +ch.obermuhlner:big-math:2.0.0:jar +ch.qos.logback:logback-classic:1.2.3:jar +ch.qos.logback:logback-core:1.2.3:jar +com.fasterxml.jackson.core:jackson-annotations:2.12.0:jar +com.fasterxml.jackson.core:jackson-core:2.12.0:jar +com.fasterxml.jackson.core:jackson-databind:2.12.0:jar +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.0:jar +com.github.javaparser:javaparser-core:3.13.10:jar +com.github.virtuald:curvesapi:1.06:jar +com.google.code.findbugs:annotations:3.0.1:jar +com.google.errorprone:error_prone_annotations:2.1.3:jar +com.google.guava:guava:25.0-jre:jar +com.google.j2objc:j2objc-annotations:1.1:jar +com.google.re2j:re2j:1.2:jar +com.googlecode.java-ipv6:java-ipv6:0.17:jar +com.hubspot.jinjava:jinjava:2.5.6:jar +com.sun.activation:jakarta.activation:1.2.2:jar +com.sun.istack:istack-commons-runtime:3.0.8:jar +com.thoughtworks.xstream:xstream:1.4.14:jar +com.zaxxer:SparseBitSet:1.2:jar +commons-codec:commons-codec:1.14:jar +commons-io:commons-io:2.8.0:jar +commons-net:commons-net:3.3:jar +info.picocli:picocli:4.6.1:jar +io.trygvis.rules-sandbox:ri-engine:1.0-SNAPSHOT:jar +io.trygvis.rules-sandbox.module:ri-module-api:1.0-SNAPSHOT:jar +jakarta.activation:jakarta.activation-api:1.2.2:jar +jakarta.xml.bind:jakarta.xml.bind-api:2.3.3:jar +org.antlr:antlr-runtime:3.5.2:jar +org.apache.commons:commons-collections4:4.4:jar +org.apache.commons:commons-compress:1.19:jar +org.apache.commons:commons-lang3:3.10:jar +org.apache.commons:commons-math3:3.4.1:jar +org.apache.poi:poi:4.1.2:jar +org.apache.poi:poi-ooxml:4.1.2:jar +org.apache.poi:poi-ooxml-schemas:4.1.2:jar +org.apache.xmlbeans:xmlbeans:3.1.0:jar +org.checkerframework:checker-compat-qual:2.0.0:jar +org.codehaus.mojo:animal-sniffer-annotations:1.14:jar +org.drools:drools-canonical-model:7.48.0.Final:jar +org.drools:drools-compiler:7.48.0.Final:jar +org.drools:drools-core:7.48.0.Final:jar +org.drools:drools-core-dynamic:7.48.0.Final:jar +org.drools:drools-core-reflective:7.48.0.Final:jar +org.drools:drools-decisiontables:7.48.0.Final:jar +org.drools:drools-ecj:7.48.0.Final:jar +org.drools:drools-model-compiler:7.48.0.Final:jar +org.drools:drools-mvel:7.48.0.Final:jar +org.drools:drools-mvel-compiler:7.48.0.Final:jar +org.drools:drools-mvel-parser:7.48.0.Final:jar +org.drools:drools-templates:7.48.0.Final:jar +org.glassfish.jaxb:jaxb-runtime:2.3.3:jar +org.glassfish.jaxb:txw2:2.3.3:jar +org.javassist:javassist:3.24.1-GA:jar +org.jsoup:jsoup:1.10.3:jar +org.kie:kie-api:7.48.0.Final:jar +org.kie:kie-internal:7.48.0.Final:jar +org.kie:kie-memory-compiler:7.48.0.Final:jar +org.kie.kogito:drools-compiler:1.1.0.Final:jar +org.kie.kogito:drools-core:1.1.0.Final:jar +org.kie.kogito:drools-core-dynamic:1.1.0.Final:jar +org.kie.kogito:drools-core-static:1.1.0.Final:jar +org.kie.kogito:kogito-api:1.1.0.Final:jar +org.kie.kogito:kogito-drools-model:1.1.0.Final:jar +org.kie.kogito:kogito-internal:1.1.0.Final:jar +org.kie.kogito:kogito-services:1.1.0.Final:jar +org.kie.soup:kie-soup-commons:7.48.0.Final:jar +org.kie.soup:kie-soup-maven-support:7.48.0.Final:jar +org.kie.soup:kie-soup-project-datamodel-api:7.48.0.Final:jar +org.kie.soup:kie-soup-project-datamodel-commons:7.48.0.Final:jar +org.kie.soup:kie-soup-xstream:7.48.0.Final:jar +org.mvel:mvel2:2.4.11.Final:jar +org.optaplanner:optaplanner-core:8.1.0.Final:jar +org.optaplanner:optaplanner-persistence-common:8.1.0.Final:jar +org.optaplanner:optaplanner-persistence-jackson:8.1.0.Final:jar +org.optaplanner:optaplanner-spring-boot-autoconfigure:8.1.0.Final:jar +org.optaplanner:optaplanner-spring-boot-starter:8.1.0.Final:jar +org.slf4j:slf4j-api:1.7.30:jar +org.springframework:spring-aop:5.2.7.RELEASE:jar +org.springframework:spring-beans:5.2.7.RELEASE:jar +org.springframework:spring-context:5.2.7.RELEASE:jar +org.springframework:spring-core:5.2.7.RELEASE:jar +org.springframework:spring-expression:5.2.7.RELEASE:jar +org.springframework:spring-jcl:5.2.7.RELEASE:jar +org.springframework.boot:spring-boot:2.3.1.RELEASE:jar +org.springframework.boot:spring-boot-autoconfigure:2.3.1.RELEASE:jar +org.yaml:snakeyaml:1.26:jar +xmlpull:xmlpull:1.1.3.1:jar +xpp3:xpp3_min:1.1.4c:jar diff --git a/src/ri-planner/pom.xml b/src/ri-planner/pom.xml new file mode 100644 index 0000000..ed8699d --- /dev/null +++ b/src/ri-planner/pom.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>io.trygvis.rules-sandbox</groupId> + <artifactId>rules-sandbox</artifactId> + <version>1.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>acme-planner</artifactId> + + <dependencies> + <dependency> + <groupId>org.optaplanner</groupId> + <artifactId>optaplanner-spring-boot-starter</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>ri-engine</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.optaplanner</groupId> + <artifactId>optaplanner-spring-boot-starter</artifactId> + <version>8.1.0.Final</version> + </dependency> + </dependencies> + </dependencyManagement> + +</project> diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java new file mode 100644 index 0000000..7311918 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java @@ -0,0 +1,48 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty; +import org.optaplanner.core.api.domain.solution.PlanningScore; +import org.optaplanner.core.api.domain.solution.PlanningSolution; +import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty; +import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; +import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; + +import java.util.List; + +@PlanningSolution +public class CloudBalance { + + private List<CloudComputer> computerList; + + private List<CloudProcess> processList; + + private HardSoftScore score; + + public CloudBalance(List<CloudComputer> computerList, List<CloudProcess> processList) { + this.computerList = computerList; + this.processList = processList; + } + + public CloudBalance() { + } + + @ValueRangeProvider(id = "computerRange") + @ProblemFactCollectionProperty + public List<CloudComputer> getComputerList() { + return computerList; + } + + @PlanningEntityCollectionProperty + public List<CloudProcess> getProcessList() { + return processList; + } + + @PlanningScore + public HardSoftScore getScore() { + return score; + } + + public void setScore(HardSoftScore score) { + this.score = score; + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java new file mode 100644 index 0000000..6a14372 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java @@ -0,0 +1,45 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; +import org.optaplanner.core.api.score.calculator.EasyScoreCalculator; + +public class CloudBalancingEasyScoreCalculator implements EasyScoreCalculator<CloudBalance, HardSoftScore> { + + @Override + public HardSoftScore calculateScore(CloudBalance cloudBalance) { + int hardScore = 0; + int softScore = 0; + for (CloudComputer computer : cloudBalance.getComputerList()) { + int cpu = 0; + int memory = 0; + boolean used = false; + + // Calculate usage + for (CloudProcess process : cloudBalance.getProcessList()) { + if (computer.equals(process.computer)) { + cpu += process.requiredCpu; + memory += process.requiredMemory; + used = true; + } + } + + var instance = computer.instance; + + // Hard constraints + int cpuPowerAvailable = instance.cpu - cpu; + if (cpuPowerAvailable < 0) { + hardScore += cpuPowerAvailable; + } + int memoryAvailable = instance.memory - memory; + if (memoryAvailable < 0) { + hardScore += memoryAvailable; + } + + // Soft constraints + if (used) { + softScore -= instance.cost; + } + } + return HardSoftScore.of(hardScore, softScore); + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java new file mode 100644 index 0000000..aa2087d --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java @@ -0,0 +1,16 @@ +package io.trygvis.acme.planning.machine; + +public class CloudComputer { + public final String host; + public final ScalewayInstance instance; + + public CloudComputer(String host, ScalewayInstance instance) { + this.host = host; + this.instance = instance; + } + + @Override + public String toString() { + return host; + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java new file mode 100644 index 0000000..60b3fd3 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java @@ -0,0 +1,52 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.solver.SolverFactory; + +import java.util.ArrayList; + +public class CloudPlaningMain { + public static final ScalewayInstance Stardust1_s = new ScalewayInstance("Stardust1-s", 0.0025, 1000, 1000); + public static final ScalewayInstance DEV1_S = new ScalewayInstance("DEV1-S", 0.01, 2000, 2000); + public static final ScalewayInstance DEV1_M = new ScalewayInstance("DEV1-M", 0.02, 3000, 4000); + public static final ScalewayInstance DEV1_L = new ScalewayInstance("DEV1-L", 0.04, 4000, 8000); + public static final ScalewayInstance DEV1_XL = new ScalewayInstance("DEV1-XL", 0.06, 4000, 12000); + + public static void main(String[] args) { + var solverFactory = SolverFactory.<CloudBalance>createFromXmlResource("io/trygvis/acme/planning/machine/solver-config.xml"); + var solver = solverFactory.buildSolver(); + + var computers = new ArrayList<CloudComputer>(); + + computers.add(new CloudComputer("acme-1", DEV1_S)); + computers.add(new CloudComputer("acme-2", DEV1_M)); + computers.add(new CloudComputer("acme-3", DEV1_S)); + + var processes = new ArrayList<CloudProcess>(); + + processes.add(new CloudProcess("statera", 200, 1000)); + processes.add(new CloudProcess("statera-console", 100, 50)); + processes.add(new CloudProcess("4tune-web", 100, 50)); + processes.add(new CloudProcess("4tune-api", 200, 200)); + processes.add(new CloudProcess("pdb", 500, 500)); + processes.add(new CloudProcess("mdb", 500, 200)); + + var unsolvedCloudBalance = new CloudBalance(computers, processes); + + var solvedCloudBalance = solver.solve(unsolvedCloudBalance); + + System.out.println("solvedCloudBalance.getScore() = " + solvedCloudBalance.getScore()); + for (CloudProcess process : solvedCloudBalance.getProcessList()) { + System.out.println("process.id = " + process.id); + if (process.computer == null) { + System.out.println("COMPUTER IS NULL"); + } else { + System.out.println("process.computer.host = " + process.computer.host); + } + } + + System.out.println("------------------------------------------------------------------------"); + +// System.out.println("\nSolved cloudBalance with 400 computers and 1200 processes:\n" +// + toDisplayString(solvedCloudBalance)); + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java new file mode 100644 index 0000000..c629656 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java @@ -0,0 +1,22 @@ +package io.trygvis.acme.planning.machine; + +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.PlanningVariable; + +@PlanningEntity() +public class CloudPlanner { + private int requiredCpuPower; + private int requiredMemory; + private int requiredNetworkBandwidth; + + private CloudComputer computer; + + @PlanningVariable(valueRangeProviderRefs = {"computerRange"}) + public CloudComputer getComputer() { + return computer; + } + + public void setComputer(CloudComputer computer) { + this.computer = computer; + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java new file mode 100644 index 0000000..2002903 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java @@ -0,0 +1,48 @@ +package io.trygvis.acme.planning.machine; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.PlanningVariable; + +import java.util.Comparator; + +@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class) +public class CloudProcess { + public String id; + public int requiredCpu; + public int requiredMemory; + public int requiredMultiplicand; + + @PlanningVariable( + valueRangeProviderRefs = "computerRange", + strengthComparatorClass = CloudComputerStrengthComparator.class) + public CloudComputer computer; + + public CloudProcess(String id, int requiredCpu, int requiredMemory) { + this.id = id; + this.requiredCpu = requiredCpu; + this.requiredMemory = requiredMemory; + + this.requiredMultiplicand = requiredCpu * requiredMemory; + } + + @SuppressWarnings("unused") + public CloudProcess() { + } + + public String toString() { + return id; + } + + public static class CloudComputerStrengthComparator implements Comparator<CloudComputer> { + public int compare(CloudComputer a, CloudComputer b) { + var x = a.instance; + var y = b.instance; + return new CompareToBuilder() + .append(x.multiplicand, y.multiplicand) + .append(y.cost, x.cost) // Descending (but this is debatable) + .append(x.kind, y.kind) + .toComparison(); + } + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java new file mode 100644 index 0000000..658e826 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java @@ -0,0 +1,15 @@ +package io.trygvis.acme.planning.machine; + +import org.apache.commons.lang3.builder.CompareToBuilder; + +import java.util.Comparator; + +public class CloudProcessDifficultyComparator implements Comparator<CloudProcess> { + + public int compare(CloudProcess a, CloudProcess b) { + return new CompareToBuilder() + .append(a.requiredMultiplicand, b.requiredMultiplicand) + .append(a.id, b.id) + .toComparison(); + } +} diff --git a/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java new file mode 100644 index 0000000..dfdc0e0 --- /dev/null +++ b/src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java @@ -0,0 +1,25 @@ +package io.trygvis.acme.planning.machine; + +public class ScalewayInstance { + public final String kind; + // Euros / hour + public final double cost; + public final int cpu; + public final int memory; + + public int multiplicand; + + public ScalewayInstance(String kind, double cost, int cpu, int memory) { + this.kind = kind; + this.cost = cost; + this.cpu = cpu; + this.memory = memory; + + this.multiplicand = cpu * memory; + } + + @Override + public String toString() { + return kind; + } +} diff --git a/src/ri-planner/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml b/src/ri-planner/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml new file mode 100644 index 0000000..81ae8ed --- /dev/null +++ b/src/ri-planner/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd"> + <!-- Domain model configuration --> + <solutionClass>io.trygvis.acme.planning.machine.CloudBalance</solutionClass> + <entityClass>io.trygvis.acme.planning.machine.CloudProcess</entityClass> + + <!-- Score configuration --> + <scoreDirectorFactory> + <easyScoreCalculatorClass>io.trygvis.acme.planning.machine.CloudBalancingEasyScoreCalculator</easyScoreCalculatorClass> + <!--<scoreDrl>org/optaplanner/examples/cloudbalancing/solver/cloudBalancingConstraints.drl</scoreDrl>--> + </scoreDirectorFactory> + + <!-- Optimization algorithms configuration --> + <termination> + <secondsSpentLimit>3</secondsSpentLimit> + </termination> +</solver> |