summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ri-engine/classpath.txt62
-rw-r--r--src/ri-engine/pom.xml159
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/DbIo.java365
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/DbObject.java16
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/Engine.java172
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/JinjavaTemplateEngine.java51
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/Main.java21
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/TemplateLoader.java7
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/DatabaseCommand.java20
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/EngineFile.java18
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/NinjaCommand.java156
-rw-r--r--src/ri-engine/src/main/java/io/trygvis/rules/engine/cli/RunCommand.java82
-rw-r--r--src/ri-it/classpath.txt63
-rw-r--r--src/ri-it/pom.xml42
-rw-r--r--src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeAppsTestMain.java20
-rw-r--r--src/ri-it/src/test/java/io/trygvis/rules/engine/AcmeWireguardTestMain.java28
-rw-r--r--src/ri-it/src/test/java/io/trygvis/rules/engine/NinjaTestMain.java16
-rw-r--r--src/ri-planner/classpath.txt90
-rw-r--r--src/ri-planner/pom.xml38
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalance.java48
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudBalancingEasyScoreCalculator.java45
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudComputer.java16
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlaningMain.java52
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudPlanner.java22
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcess.java48
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/CloudProcessDifficultyComparator.java15
-rw-r--r--src/ri-planner/src/main/java/io/trygvis/acme/planning/machine/ScalewayInstance.java25
-rw-r--r--src/ri-planner/src/main/resources/io/trygvis/acme/planning/machine/solver-config.xml18
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>