diff options
author | 2023-12-24 17:28:11 +0100 | |
---|---|---|
committer | 2023-12-24 17:41:29 +0100 | |
commit | f7c9414c4bcc8a48cbc1c25879e9c8eafe772320 (patch) | |
tree | b3adee36064e0dcf1fefac89466b41cfc52e631b /subprojects/generator-cli | |
parent | chore(deps): bump dependencies (diff) | |
download | refinery-f7c9414c4bcc8a48cbc1c25879e9c8eafe772320.tar.gz refinery-f7c9414c4bcc8a48cbc1c25879e9c8eafe772320.tar.zst refinery-f7c9414c4bcc8a48cbc1c25879e9c8eafe772320.zip |
feat: command line model generator
Diffstat (limited to 'subprojects/generator-cli')
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 | |||
7 | plugins { | ||
8 | id("tools.refinery.gradle.java-application") | ||
9 | } | ||
10 | |||
11 | dependencies { | ||
12 | implementation(project(":refinery-generator")) | ||
13 | implementation(libs.jcommander) | ||
14 | implementation(libs.slf4j.api) | ||
15 | } | ||
16 | |||
17 | application { | ||
18 | mainClass.set("tools.refinery.generator.cli.RefineryCli") | ||
19 | } | ||
20 | |||
21 | tasks.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 | */ | ||
6 | package tools.refinery.generator.cli; | ||
7 | |||
8 | import com.beust.jcommander.JCommander; | ||
9 | import com.beust.jcommander.ParameterException; | ||
10 | import com.google.inject.Inject; | ||
11 | import tools.refinery.generator.cli.commands.GenerateCommand; | ||
12 | import tools.refinery.generator.standalone.StandaloneRefinery; | ||
13 | |||
14 | import java.io.IOException; | ||
15 | |||
16 | public 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 | */ | ||
6 | package tools.refinery.generator.cli.commands; | ||
7 | |||
8 | import com.beust.jcommander.Parameter; | ||
9 | import com.beust.jcommander.Parameters; | ||
10 | import com.google.inject.Inject; | ||
11 | import org.eclipse.emf.ecore.resource.Resource; | ||
12 | import tools.refinery.generator.ModelGeneratorFactory; | ||
13 | import tools.refinery.generator.ProblemLoader; | ||
14 | import tools.refinery.language.model.problem.Problem; | ||
15 | import tools.refinery.language.model.problem.Relation; | ||
16 | import tools.refinery.language.model.problem.ScopeDeclaration; | ||
17 | |||
18 | import java.io.ByteArrayOutputStream; | ||
19 | import java.io.FileOutputStream; | ||
20 | import java.io.IOException; | ||
21 | import java.nio.charset.StandardCharsets; | ||
22 | import java.util.ArrayList; | ||
23 | import java.util.HashSet; | ||
24 | import java.util.List; | ||
25 | import java.util.Map; | ||
26 | |||
27 | @Parameters(commandDescription = "Generate a model from a partial model") | ||
28 | public 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 | } | ||