Skip to content

Commit 57c52ca

Browse files
committed
Add shell output templates
1 parent de98ba9 commit 57c52ca

File tree

7 files changed

+261
-3
lines changed

7 files changed

+261
-3
lines changed

code/cli/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979
<artifactId>commons-text</artifactId>
8080
</dependency>
8181

82+
<dependency>
83+
<groupId>io.pebbletemplates</groupId>
84+
<artifactId>pebble</artifactId>
85+
</dependency>
86+
8287
<dependency>
8388
<groupId>info.picocli</groupId>
8489
<artifactId>picocli</artifactId>

code/cli/src/main/java/io/projectenv/core/cli/command/ProjectEnvInstallCommand.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,33 @@
44
import io.projectenv.core.cli.ToolSupportHelper;
55
import io.projectenv.core.cli.configuration.ProjectEnvConfiguration;
66
import io.projectenv.core.cli.parser.ToolInfoParser;
7+
import io.projectenv.core.cli.shell.TemplateProcessor;
78
import io.projectenv.core.commons.process.ProcessOutput;
89
import io.projectenv.core.toolsupport.spi.ToolInfo;
910
import io.projectenv.core.toolsupport.spi.ToolSupport;
1011
import io.projectenv.core.toolsupport.spi.ToolSupportContext;
1112
import io.projectenv.core.toolsupport.spi.ToolSupportException;
13+
import org.apache.commons.io.FileUtils;
14+
import org.apache.commons.lang3.SystemUtils;
1215
import picocli.CommandLine.Command;
16+
import picocli.CommandLine.Option;
1317

18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
1421
import java.util.*;
1522

1623
@Command(name = "install")
1724
public class ProjectEnvInstallCommand extends AbstractProjectEnvCliCommand {
1825

26+
@Option(names = {"--output-template"})
27+
private String outputTemplate;
28+
29+
@Option(names = {"--output-file"})
30+
private File outputFile;
31+
1932
@Override
20-
protected void callInternal(ProjectEnvConfiguration configuration, ToolSupportContext toolSupportContext) {
33+
protected void callInternal(ProjectEnvConfiguration configuration, ToolSupportContext toolSupportContext) throws IOException {
2134
writeOutput(installOrUpdateTools(configuration, toolSupportContext));
2235
}
2336

@@ -54,8 +67,33 @@ private <T> List<ToolInfo> installOrUpdateTool(ToolSupport<T> toolSupport, Proje
5467
return toolInfos;
5568
}
5669

57-
private void writeOutput(Map<String, List<ToolInfo>> toolInfos) {
58-
ProcessOutput.writeResult(ToolInfoParser.toJson(toolInfos));
70+
private void writeOutput(Map<String, List<ToolInfo>> toolInfos) throws IOException {
71+
String content = prepareOutput(toolInfos);
72+
if (outputFile != null) {
73+
writeOutputToFile(content, outputFile);
74+
} else {
75+
writeOutputToStdOutput(content);
76+
}
77+
}
78+
79+
private String prepareOutput(Map<String, List<ToolInfo>> toolInfos) throws IOException {
80+
if (outputTemplate == null) {
81+
return ToolInfoParser.toJson(toolInfos);
82+
} else {
83+
return TemplateProcessor.processTemplate(outputTemplate, toolInfos);
84+
}
85+
}
86+
87+
private void writeOutputToFile(String content, File target) throws IOException {
88+
FileUtils.write(target, content, StandardCharsets.UTF_8);
89+
90+
if (!SystemUtils.IS_OS_WINDOWS && !target.setExecutable(true)) {
91+
ProcessOutput.writeInfoMessage("failed to make file {0} executable", target.getCanonicalPath());
92+
}
93+
}
94+
95+
private void writeOutputToStdOutput(String content) {
96+
ProcessOutput.writeResult(content);
5997
}
6098

6199
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package io.projectenv.core.cli.shell;
2+
3+
import io.pebbletemplates.pebble.PebbleEngine;
4+
import io.pebbletemplates.pebble.extension.AbstractExtension;
5+
import io.pebbletemplates.pebble.extension.Filter;
6+
import io.pebbletemplates.pebble.template.EvaluationContext;
7+
import io.pebbletemplates.pebble.template.PebbleTemplate;
8+
import io.projectenv.core.toolsupport.spi.ToolInfo;
9+
import org.apache.commons.lang3.ClassPathUtils;
10+
import org.apache.commons.lang3.StringUtils;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.io.StringWriter;
15+
import java.io.Writer;
16+
import java.util.Collections;
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.regex.Pattern;
21+
22+
public final class TemplateProcessor {
23+
24+
public static final String PEBBLE_TEMPLATE_EXT = ".peb";
25+
26+
private static final PebbleEngine PEBBLE_ENGINE = new PebbleEngine
27+
.Builder()
28+
.strictVariables(false)
29+
.extension(new PebbleExtension())
30+
.build();
31+
32+
private TemplateProcessor() {
33+
// noop
34+
}
35+
36+
public static String processTemplate(String template, Map<String, List<ToolInfo>> toolInfos) throws IOException {
37+
PebbleTemplate compiledTemplate = PEBBLE_ENGINE.getTemplate(resolveTemplate(template));
38+
39+
Writer writer = new StringWriter();
40+
41+
var context = new HashMap<String, Object>();
42+
context.put("toolInfos", toolInfos);
43+
44+
compiledTemplate.evaluate(writer, context);
45+
46+
return writer.toString();
47+
}
48+
49+
private static String resolveTemplate(String template) {
50+
var templateFile = new File(template);
51+
if (templateFile.exists()) {
52+
return template;
53+
}
54+
55+
if (StringUtils.endsWith(template, PEBBLE_TEMPLATE_EXT)) {
56+
return ClassPathUtils.toFullyQualifiedPath(TemplateProcessor.class, template);
57+
}
58+
59+
return ClassPathUtils.toFullyQualifiedPath(TemplateProcessor.class, template + PEBBLE_TEMPLATE_EXT);
60+
}
61+
62+
private static class PebbleExtension extends AbstractExtension {
63+
64+
@Override
65+
public Map<String, Filter> getFilters() {
66+
return Map.of("path", new PathFilter());
67+
}
68+
69+
}
70+
71+
private static class PathFilter implements Filter {
72+
73+
@Override
74+
public List<String> getArgumentNames() {
75+
return Collections.emptyList();
76+
}
77+
78+
@Override
79+
public Object apply(Object input, Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
80+
try {
81+
if (input instanceof File file) {
82+
String canonicalPath = file.getCanonicalPath();
83+
84+
// removes a trailing path separator if existing
85+
canonicalPath = canonicalPath.replaceAll(Pattern.quote(File.separator) + "$", "");
86+
87+
// replaces all back-slashes with forward-slashes
88+
canonicalPath = canonicalPath.replaceAll(Pattern.quote("\\"), "/");
89+
90+
return canonicalPath;
91+
} else {
92+
return input;
93+
}
94+
} catch (IOException e) {
95+
throw new IllegalArgumentException("invalid file", e);
96+
}
97+
}
98+
99+
}
100+
101+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/sh
2+
{% for entry in toolInfos.entrySet() %}
3+
{% for toolInfo in entry.value %}
4+
5+
{% for environmentVariable in toolInfo.environmentVariables %}
6+
export {{ environmentVariable.key }}="$(cygpath '{{ environmentVariable.value | path }}')"
7+
{% endfor %}
8+
9+
{% for pathElement in toolInfo.pathElements %}
10+
export PATH="$(cygpath '{{ pathElement | path }}'):$PATH"
11+
{% endfor %}
12+
13+
{% if entry.key == "maven" %}
14+
{% if toolInfo.unhandledProjectResources.userSettingsFile != null %}
15+
alias mvn="mvn -s {{ toolInfo.unhandledProjectResources.userSettingsFile | path }}"
16+
{% endif %}
17+
{% endif %}
18+
19+
{% endfor %}
20+
{% endfor %}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/sh
2+
{% for entry in toolInfos.entrySet() %}
3+
{% for toolInfo in entry.value %}
4+
5+
{% for environmentVariable in toolInfo.environmentVariables %}
6+
export {{ environmentVariable.key }}="{{ environmentVariable.value | path }}"
7+
{% endfor %}
8+
9+
{% for pathElement in toolInfo.pathElements %}
10+
export PATH="{{ pathElement | path }}:$PATH"
11+
{% endfor %}
12+
13+
{% if entry.key == "maven" %}
14+
{% if toolInfo.unhandledProjectResources.userSettingsFile != null %}
15+
alias mvn="mvn -s {{ toolInfo.unhandledProjectResources.userSettingsFile | path }}"
16+
{% endif %}
17+
{% endif %}
18+
19+
{% endfor %}
20+
{% endfor %}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.projectenv.core.cli.shell;
2+
3+
import org.apache.commons.io.FileUtils;
4+
import org.apache.commons.lang3.StringUtils;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.io.TempDir;
7+
8+
import java.io.File;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Map;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class TemplateProcessorTest {
15+
16+
@Test
17+
void testFileTemplate(@TempDir File tempDir) throws Exception {
18+
var expectedContent = "custom-template";
19+
20+
var customTemplate = createTemplateInDirectory(expectedContent, tempDir);
21+
var actualContent = TemplateProcessor.processTemplate(customTemplate, Map.of());
22+
assertThat(actualContent).isEqualTo(expectedContent);
23+
}
24+
25+
@Test
26+
void testClasspathTemplateWithExtension() throws Exception {
27+
var expectedContent = "custom-template";
28+
29+
var customTemplate = createTemplateInTemplatesClasspathDirectory(expectedContent);
30+
assertThat(customTemplate).endsWith(TemplateProcessor.PEBBLE_TEMPLATE_EXT);
31+
32+
var actualContent = TemplateProcessor.processTemplate(customTemplate, Map.of());
33+
assertThat(actualContent).isEqualTo(expectedContent);
34+
}
35+
36+
@Test
37+
void testClasspathTemplateWithoutExtension() throws Exception {
38+
var expectedContent = "custom-template";
39+
40+
var customTemplate = createTemplateInTemplatesClasspathDirectory(expectedContent);
41+
assertThat(customTemplate).endsWith(TemplateProcessor.PEBBLE_TEMPLATE_EXT);
42+
43+
customTemplate = StringUtils.remove(customTemplate, TemplateProcessor.PEBBLE_TEMPLATE_EXT);
44+
assertThat(customTemplate).doesNotEndWith(TemplateProcessor.PEBBLE_TEMPLATE_EXT);
45+
46+
var actualContent = TemplateProcessor.processTemplate(customTemplate, Map.of());
47+
assertThat(actualContent).isEqualTo(expectedContent);
48+
}
49+
50+
private String createTemplateInDirectory(String templateContent, File parentDirectory) throws Exception {
51+
var customTemplate = File.createTempFile("custom-template", ".peb", parentDirectory);
52+
FileUtils.write(customTemplate, templateContent, StandardCharsets.UTF_8);
53+
54+
return customTemplate.getAbsolutePath();
55+
}
56+
57+
private String createTemplateInTemplatesClasspathDirectory(String templateContent) throws Exception {
58+
var customTemplate = File.createTempFile("custom-template", ".peb", getTemplatesClasspathLocation());
59+
FileUtils.write(customTemplate, templateContent, StandardCharsets.UTF_8);
60+
61+
return customTemplate.getName();
62+
}
63+
64+
private File getTemplatesClasspathLocation() throws Exception {
65+
return new File(getClass().getResource(".").toURI());
66+
}
67+
68+
}

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<commons-text.version>1.12.0</commons-text.version>
4545
<graal-sdk.version>23.1.4</graal-sdk.version>
4646
<slf4j.version>2.0.16</slf4j.version>
47+
<pebble.version>3.2.0</pebble.version>
4748

4849
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
4950
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
@@ -178,6 +179,11 @@
178179
<artifactId>graal-sdk</artifactId>
179180
<version>${graal-sdk.version}</version>
180181
</dependency>
182+
<dependency>
183+
<groupId>io.pebbletemplates</groupId>
184+
<artifactId>pebble</artifactId>
185+
<version>${pebble.version}</version>
186+
</dependency>
181187
</dependencies>
182188
</dependencyManagement>
183189

0 commit comments

Comments
 (0)