aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/generator-cli
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-12-24 17:28:11 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-12-24 17:41:29 +0100
commitf7c9414c4bcc8a48cbc1c25879e9c8eafe772320 (patch)
treeb3adee36064e0dcf1fefac89466b41cfc52e631b /subprojects/generator-cli
parentchore(deps): bump dependencies (diff)
downloadrefinery-f7c9414c4bcc8a48cbc1c25879e9c8eafe772320.tar.gz
refinery-f7c9414c4bcc8a48cbc1c25879e9c8eafe772320.tar.zst
refinery-f7c9414c4bcc8a48cbc1c25879e9c8eafe772320.zip
feat: command line model generator
Diffstat (limited to 'subprojects/generator-cli')
-rw-r--r--subprojects/generator-cli/build.gradle.kts24
-rw-r--r--subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java64
-rw-r--r--subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java141
3 files changed, 229 insertions, 0 deletions
diff --git a/subprojects/generator-cli/build.gradle.kts b/subprojects/generator-cli/build.gradle.kts
new file mode 100644
index 00000000..6c681222
--- /dev/null
+++ b/subprojects/generator-cli/build.gradle.kts
@@ -0,0 +1,24 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7plugins {
8 id("tools.refinery.gradle.java-application")
9}
10
11dependencies {
12 implementation(project(":refinery-generator"))
13 implementation(libs.jcommander)
14 implementation(libs.slf4j.api)
15}
16
17application {
18 mainClass.set("tools.refinery.generator.cli.RefineryCli")
19}
20
21tasks.shadowJar {
22 // Silence Xtext warning.
23 append("plugin.properties")
24}
diff --git a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java
new file mode 100644
index 00000000..5de579e6
--- /dev/null
+++ b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java
@@ -0,0 +1,64 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.cli;
7
8import com.beust.jcommander.JCommander;
9import com.beust.jcommander.ParameterException;
10import com.google.inject.Inject;
11import tools.refinery.generator.cli.commands.GenerateCommand;
12import tools.refinery.generator.standalone.StandaloneRefinery;
13
14import java.io.IOException;
15
16public class RefineryCli {
17 private static final String GENERATE_COMMAND = "generate";
18
19 @Inject
20 private GenerateCommand generateCommand;
21
22 private JCommander jCommander;
23
24 public String parseArguments(String... args) {
25 var jc = getJCommander();
26 jc.parse(args);
27 return jc.getParsedCommand();
28 }
29
30 public void run(String command) throws IOException {
31 switch (command) {
32 case GENERATE_COMMAND -> generateCommand.run();
33 case null, default -> showUsageAndExit();
34 }
35 }
36
37 public void showUsageAndExit() {
38 getJCommander().usage();
39 System.exit(1);
40 }
41
42 private JCommander getJCommander() {
43 if (jCommander == null) {
44 jCommander = JCommander.newBuilder()
45 .programName("refinery")
46 .addObject(this)
47 .addCommand(GENERATE_COMMAND, generateCommand)
48 .build();
49 }
50 return jCommander;
51 }
52
53 public static void main(String[] args) throws IOException {
54 var cli = StandaloneRefinery.getInjector().getInstance(RefineryCli.class);
55 String command = null;
56 try {
57 command = cli.parseArguments(args);
58 } catch (ParameterException e) {
59 System.err.println(e.getMessage());
60 cli.showUsageAndExit();
61 }
62 cli.run(command);
63 }
64}
diff --git a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java
new file mode 100644
index 00000000..6c1d105d
--- /dev/null
+++ b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java
@@ -0,0 +1,141 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.cli.commands;
7
8import com.beust.jcommander.Parameter;
9import com.beust.jcommander.Parameters;
10import com.google.inject.Inject;
11import org.eclipse.emf.ecore.resource.Resource;
12import tools.refinery.generator.ModelGeneratorFactory;
13import tools.refinery.generator.ProblemLoader;
14import tools.refinery.language.model.problem.Problem;
15import tools.refinery.language.model.problem.Relation;
16import tools.refinery.language.model.problem.ScopeDeclaration;
17
18import java.io.ByteArrayOutputStream;
19import java.io.FileOutputStream;
20import java.io.IOException;
21import java.nio.charset.StandardCharsets;
22import java.util.ArrayList;
23import java.util.HashSet;
24import java.util.List;
25import java.util.Map;
26
27@Parameters(commandDescription = "Generate a model from a partial model")
28public class GenerateCommand {
29 @Inject
30 private ProblemLoader loader;
31
32 @Inject
33 private ModelGeneratorFactory generatorFactory;
34
35 private String problemPath;
36 private String outputPath = "-";
37 private List<String> scopes = new ArrayList<>();
38 private List<String> overrideScopes = new ArrayList<>();
39 private long randomSeed = 1;
40
41 @Parameter(description = "Input path", required = true)
42 public void setProblemPath(String problemPath) {
43 this.problemPath = problemPath;
44 }
45
46 @Parameter(names = {"-output", "-o"}, description = "Output path")
47 public void setOutputPath(String outputPath) {
48 this.outputPath = outputPath;
49 }
50
51 @Parameter(names = {"-scope", "-s"}, description = "Extra scope constraints")
52 public void setScopes(List<String> scopes) {
53 this.scopes = scopes;
54 }
55
56 @Parameter(names = {"-scope-override", "-S"}, description = "Override scope constraints")
57 public void setOverrideScopes(List<String> overrideScopes) {
58 this.overrideScopes = overrideScopes;
59 }
60
61 @Parameter(names = {"-random-seed", "-r"}, description = "Random seed")
62 public void setRandomSeed(long randomSeed) {
63 this.randomSeed = randomSeed;
64 }
65
66 public void run() throws IOException {
67 var problem = addScopeConstraints(loader.loadFile(problemPath));
68 var generator = generatorFactory.createGenerator(problem);
69 generator.setRandomSeed(randomSeed);
70 generator.generate();
71 var solution = generator.serializeSolution();
72 var solutionResource = solution.eResource();
73 var saveOptions = Map.of();
74 if (outputPath == null || outputPath.equals("-")) {
75 printSolution(solutionResource, saveOptions);
76 } else {
77 try (var outputStream = new FileOutputStream(outputPath)) {
78 solutionResource.save(outputStream, saveOptions);
79 }
80 }
81 }
82
83 private Problem addScopeConstraints(Problem problem) throws IOException {
84 var allScopes = new ArrayList<>(scopes);
85 allScopes.addAll(overrideScopes);
86 if (allScopes.isEmpty()) {
87 return problem;
88 }
89 int originalStatementCount = problem.getStatements().size();
90 var builder = new StringBuilder();
91 var problemResource = problem.eResource();
92 try (var outputStream = new ByteArrayOutputStream()) {
93 problemResource.save(outputStream, Map.of());
94 builder.append(outputStream.toString(StandardCharsets.UTF_8));
95 }
96 builder.append('\n');
97 for (var scope : allScopes) {
98 builder.append("scope ").append(scope).append(".\n");
99 }
100 var modifiedProblem = loader.loadString(builder.toString(), problemResource.getURI());
101 var modifiedStatements = modifiedProblem.getStatements();
102 int modifiedStatementCount = modifiedStatements.size();
103 if (modifiedStatementCount != originalStatementCount + allScopes.size()) {
104 throw new IllegalStateException("Failed to parse scope constraints");
105 }
106 // Override scopes remove any scope constraint from the original problem with the same target type.
107 var overriddenScopes = new HashSet<Relation>();
108 for (int i = modifiedStatementCount - overrideScopes.size(); i < modifiedStatementCount; i++) {
109 var statement = modifiedStatements.get(i);
110 if (!(statement instanceof ScopeDeclaration scopeDeclaration)) {
111 throw new IllegalStateException("Invalid scope constraint: " + statement);
112 }
113 for (var typeScope : scopeDeclaration.getTypeScopes()) {
114 overriddenScopes.add(typeScope.getTargetType());
115 }
116 }
117 int statementIndex = 0;
118 var iterator = modifiedStatements.iterator();
119 // Scope overrides only affect type scopes from the original problem and leave type scopes added on the
120 // command line intact.
121 while (statementIndex < originalStatementCount && iterator.hasNext()) {
122 var statement = iterator.next();
123 if (statement instanceof ScopeDeclaration scopeDeclaration) {
124 var typeScopes = scopeDeclaration.getTypeScopes();
125 typeScopes.removeIf(typeScope -> overriddenScopes.contains(typeScope.getTargetType()));
126 // Scope declarations with no type scopes are invalid, so we have to remove them.
127 if (typeScopes.isEmpty()) {
128 iterator.remove();
129 }
130 }
131 statementIndex++;
132 }
133 return modifiedProblem;
134 }
135
136 // We deliberately write to the standard output if no output path is specified.
137 @SuppressWarnings("squid:S106")
138 private void printSolution(Resource solutionResource, Map<?, ?> saveOptions) throws IOException {
139 solutionResource.save(System.out, saveOptions);
140 }
141}