Skip to content

Commit

Permalink
add cli support for adhoc plans and pre-install files
Browse files Browse the repository at this point in the history
  • Loading branch information
adamcin committed Apr 27, 2020
1 parent c6503fd commit a737437
Show file tree
Hide file tree
Showing 26 changed files with 627 additions and 101 deletions.
17 changes: 17 additions & 0 deletions api/src/main/java/net/adamcin/oakpal/api/JavaxJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
import static net.adamcin.oakpal.api.Fun.compose;

/**
Expand Down Expand Up @@ -266,6 +267,22 @@ public static List<Object> unwrapArray(final JsonArray jsonArray) {
return jsonArray.getValuesAs(JavaxJson::unwrap);
}

/**
* Merge an overlay json object's entries into a base json object, replacing values
* for duplicate keys.
*
* @param base the base json object
* @param overlay the overlay json object
* @return a merged json object
*/
public static @NotNull JsonObject shallowMergeObjects(final @Nullable JsonObject base,
final @Nullable JsonObject overlay) {
JsonObjectBuilder init = Json.createObjectBuilder();
ofNullable(base).ifPresent(json -> json.forEach(init::add));
ofNullable(overlay).ifPresent(json -> json.forEach(init::add));
return init.build();
}

/**
* Discrete value wrapper. Delegates to {@link #wrap(Object)} for null-handling, etc.
*/
Expand Down
48 changes: 48 additions & 0 deletions api/src/test/java/net/adamcin/oakpal/api/JavaxJsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import static net.adamcin.oakpal.api.JavaxJson.mapArrayOfObjects;
import static net.adamcin.oakpal.api.JavaxJson.mapArrayOfStrings;
import static net.adamcin.oakpal.api.JavaxJson.mapObjectValues;
import static net.adamcin.oakpal.api.JavaxJson.shallowMergeObjects;
import static net.adamcin.oakpal.api.JavaxJson.obj;
import static net.adamcin.oakpal.api.JavaxJson.objectOrEmpty;
import static net.adamcin.oakpal.api.JavaxJson.optArray;
Expand Down Expand Up @@ -628,4 +629,51 @@ public void testHasNonNull() {
assertTrue("hasNonNull for key with non-null json value",
hasNonNull(key("foo", "bar").get(), "foo"));
}

@Test
public void testShallowMergeObjects() {
assertEquals("expect empty object from nulls", JsonObject.EMPTY_JSON_OBJECT,
JavaxJson.shallowMergeObjects(null, null));

assertEquals("expect empty object from null and empty", JsonObject.EMPTY_JSON_OBJECT,
JavaxJson.shallowMergeObjects(null, JsonValue.EMPTY_JSON_OBJECT));

assertEquals("expect empty object from empty and null", JsonObject.EMPTY_JSON_OBJECT,
JavaxJson.shallowMergeObjects(JsonValue.EMPTY_JSON_OBJECT, null));

final JsonObject base = obj()
.key("baseKey", "unmodified")
.key("keyString", "base")
.key("keyObject", key("baseKey", "unmodified").key("keyString", "base"))
.get();

assertEquals("expect equal base for null overlay", base, shallowMergeObjects(base, null));
assertEquals("expect equal base for empty overlay", base, shallowMergeObjects(base,
JsonValue.EMPTY_JSON_OBJECT));
assertEquals("expect equal base as overlay with null base", base, shallowMergeObjects(null, base));
assertEquals("expect equal base as overlay with empty base", base,
shallowMergeObjects(JsonValue.EMPTY_JSON_OBJECT, base));

final JsonObject overlay = obj()
.key("overlayKey", "unmodified")
.key("keyString", "overlay")
.key("keyObject", key("overlayKey", "unmodified").key("keyString", "overlay"))
.get();

assertEquals("expect merged base <- overlay", obj()
.key("baseKey", "unmodified")
.key("overlayKey", "unmodified")
.key("keyString", "overlay")
.key("keyObject", key("overlayKey", "unmodified").key("keyString", "overlay"))
.get(),
shallowMergeObjects(base, overlay));

assertEquals("expect merged overlay <- base", obj()
.key("baseKey", "unmodified")
.key("overlayKey", "unmodified")
.key("keyString", "base")
.key("keyObject", key("baseKey", "unmodified").key("keyString", "base"))
.get(),
shallowMergeObjects(overlay, base));
}
}
24 changes: 24 additions & 0 deletions cli/src/main/java/net/adamcin/oakpal/cli/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -200,6 +201,29 @@ Optional<String> flipOpt(final @NotNull String wholeOpt) {
builder.setNoPlan(isNoOpt);
builder.setPlanName(isNoOpt ? null : args[++i]);
break;
case "-pf":
case "--plan-from-file":
builder.setPlanFromFile(isNoOpt ? null : console.getCwd().toPath().resolve(args[++i]).toFile());
break;
case "--plan-from-file-base":
builder.setPlanFromFileBaseDir(isNoOpt ? null : console.getCwd().toPath().resolve(args[++i]).toFile());
break;
case "-pi":
case "--pre-install-file":
if (isNoOpt) {
builder.setPreInstallFiles(Collections.emptyList());
} else {
builder.addPreInstallFile(console.getCwd().toPath().resolve(args[++i]).toFile());
}
break;
case "-xp":
case "--extend-classpath":
if (isNoOpt) {
builder.setExtendedClassPathFiles(Collections.emptyList());
} else {
builder.addExtendedClassPathFile(console.getCwd().toPath().resolve(args[++i]).toFile());
}
break;
case "-s":
case "--severity-fail":
if (isNoOpt) {
Expand Down
155 changes: 131 additions & 24 deletions cli/src/main/java/net/adamcin/oakpal/cli/Options.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package net.adamcin.oakpal.cli;

import net.adamcin.oakpal.api.Nothing;
import net.adamcin.oakpal.api.Result;
import net.adamcin.oakpal.api.Severity;
import net.adamcin.oakpal.core.InstallHookPolicy;
import net.adamcin.oakpal.api.Nothing;
import net.adamcin.oakpal.core.OakpalPlan;
import net.adamcin.oakpal.core.OpearFile;
import net.adamcin.oakpal.api.Result;
import net.adamcin.oakpal.core.opear.AdhocOpear;
import net.adamcin.oakpal.core.opear.Opear;
import net.adamcin.oakpal.core.opear.OpearFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

import static net.adamcin.oakpal.api.Fun.compose;
import static net.adamcin.oakpal.api.Fun.result1;

final class Options {
static final String CACHE_DIR_NAME = ".oakpal-cache";
Expand All @@ -30,6 +38,10 @@ final class Options {
private final ClassLoader scanClassLoader;
private final File cacheDir;
private final File opearFile;
private final File planFromFile;
private final File planFromFileBaseDir;
private final List<File> preInstallFiles;
private final List<File> extendedClassPathFiles;
private final String planName;
private final boolean noHooks;
private final List<File> scanFiles;
Expand All @@ -40,7 +52,9 @@ final class Options {
this(true, true, false,
OakpalPlan.BASIC_PLAN_URL, Options.class.getClassLoader(),
new File(System.getProperty("java.io.tmpdir")),
null, null, false,
null, null, null, null,
Collections.emptyList(),
Collections.emptyList(), false,
Collections.emptyList(),
EMPTY_PRINTER,
Severity.MAJOR);
Expand All @@ -54,6 +68,10 @@ final class Options {
final @NotNull File cacheDir,
final @Nullable File opearFile,
final @Nullable String planName,
final @Nullable File planFromFile,
final @Nullable File planFromFileBaseDir,
final @NotNull List<File> preInstallFiles,
final @NotNull List<File> extendedClassPathFiles,
final boolean noHooks,
final @NotNull List<File> scanFiles,
final @NotNull Function<StructuredMessage, IO<Nothing>> printer,
Expand All @@ -66,6 +84,10 @@ final class Options {
this.cacheDir = cacheDir;
this.opearFile = opearFile;
this.planName = planName;
this.planFromFile = planFromFile;
this.planFromFileBaseDir = planFromFileBaseDir;
this.preInstallFiles = preInstallFiles;
this.extendedClassPathFiles = extendedClassPathFiles;
this.noHooks = noHooks;
this.scanFiles = scanFiles;
this.printer = printer;
Expand Down Expand Up @@ -108,6 +130,22 @@ public File getCacheDir() {
return planName;
}

public @Nullable File getPlanFromFile() {
return planFromFile;
}

public @Nullable File getPlanFromFileBaseDir() {
return planFromFileBaseDir;
}

public @NotNull List<File> getPreInstallFiles() {
return preInstallFiles;
}

public @NotNull List<File> getExtendedClassPathFiles() {
return extendedClassPathFiles;
}

public List<File> getScanFiles() {
return scanFiles;
}
Expand All @@ -121,13 +159,19 @@ public Severity getFailOnSeverity() {
}

boolean hasOverrides() {
return noHooks;
return noHooks || !getPreInstallFiles().isEmpty();
}

public OakpalPlan applyOverrides(final @NotNull OakpalPlan basePlan) {
if (hasOverrides()) {
final OakpalPlan.Builder overridePlan = new OakpalPlan.Builder(basePlan.getBase(), basePlan.getName())
.startingWithPlan(basePlan);
if (!getPreInstallFiles().isEmpty()) {
List<URL> allUrls = new ArrayList<>(basePlan.getPreInstallUrls());
getPreInstallFiles().stream().map(compose(File::toURI, result1(URI::toURL)))
.collect(Result.tryCollect(Collectors.toList())).forEach(allUrls::addAll);
overridePlan.withPreInstallUrls(allUrls);
}
if (isNoHooks()) {
overridePlan.withInstallHookPolicy(InstallHookPolicy.SKIP);
overridePlan.withEnablePreInstallHooks(false);
Expand All @@ -146,6 +190,10 @@ static final class Builder {
private boolean noPlan;
private boolean noHooks;
private String planName;
private File planFromFile;
private File planFromFileBaseDir;
private List<File> preInstallFiles = new ArrayList<>();
private List<File> extendedClassPathFiles = new ArrayList<>();
private File outFile;
private File cacheDir;
private File opearFile;
Expand Down Expand Up @@ -187,6 +235,36 @@ public Builder setPlanName(final @Nullable String planName) {
return this;
}

public Builder setPlanFromFileBaseDir(final @Nullable File planFromFileBaseDir) {
this.planFromFileBaseDir = planFromFileBaseDir;
return this;
}

public Builder setPlanFromFile(final @Nullable File planFromFile) {
this.planFromFile = planFromFile;
return this;
}

public Builder setPreInstallFiles(final @NotNull List<File> preInstallFiles) {
this.preInstallFiles = preInstallFiles;
return this;
}

public Builder addPreInstallFile(final @NotNull File preInstallFile) {
this.preInstallFiles.add(preInstallFile);
return this;
}

public Builder setExtendedClassPathFiles(final @NotNull List<File> extendedClassPathFiles) {
this.extendedClassPathFiles = new ArrayList<>(extendedClassPathFiles);
return this;
}

public Builder addExtendedClassPathFile(final @NotNull File extendedClassPathFile) {
this.extendedClassPathFiles.add(extendedClassPathFile);
return this;
}

public Builder setOutFile(final @Nullable File outFile) {
this.outFile = outFile;
return this;
Expand All @@ -212,19 +290,27 @@ public Builder setFailOnSeverity(final @Nullable Severity failOnSeverity) {
return this;
}

Result<Options> build(final @NotNull Console console) {
Result<Opear> buildOpear(final @NotNull Console console, final @NotNull File opearCache) {
final Result<Opear> baseOpear;
if (planFromFile != null) {
baseOpear = buildAdhocOpear(console).map(Function.identity());
} else {
baseOpear = buildOpearFile(console, opearCache).map(Function.identity());
}
return baseOpear;
}

Result<AdhocOpear> buildAdhocOpear(final @NotNull Console console) {
return AdhocOpear.fromPlanFile(planFromFile, planFromFileBaseDir);
}

Result<OpearFile> buildOpearFile(final @NotNull Console console, final @NotNull File opearCache) {
final File opearResolved = Optional.ofNullable(opearFile).orElseGet(() ->
console.getCwd().toPath().resolve(
console.getEnv().getOrDefault(Console.ENV_OAKPAL_OPEAR, "."))
.toFile());

final File realCacheDir = this.cacheDir != null
? this.cacheDir
: console.getCwd().toPath().resolve(CACHE_DIR_NAME).toFile().getAbsoluteFile();
final File opearCache = new File(realCacheDir, "opears");
opearCache.mkdirs();

final Result<OpearFile> opearResult = Result.success(opearResolved.getAbsoluteFile())
return Result.success(opearResolved.getAbsoluteFile())
.flatMap(file -> {
if (file.isFile()) {
try (JarFile jarFile = new JarFile(file, true)) {
Expand All @@ -236,20 +322,41 @@ Result<Options> build(final @NotNull Console console) {
return OpearFile.fromDirectory(file);
}
});
}

return opearResult.flatMap(opear ->
Optional.ofNullable(planName)
.map(opear::getSpecificPlan)
.orElse(Result.success(noPlan ? OakpalPlan.EMPTY_PLAN_URL : opear.getDefaultPlan()))
.flatMap(planUrl ->
messageWriter(console, outputJson, outFile).map(writer ->
new Options(justHelp, justVersion, storeBlobs, planUrl,
opear.getPlanClassLoader(getClass().getClassLoader()),
realCacheDir, opearFile,
planName, noHooks, scanFiles, writer,
Optional.ofNullable(failOnSeverity).orElse(DEFAULT_OPTIONS.failOnSeverity)))));
Result<ClassLoader> getExtendedClassLoader(final @NotNull Opear opear,
final @NotNull ClassLoader parentClassLoader) {
if (this.extendedClassPathFiles.isEmpty()) {
return Result.success(opear.getPlanClassLoader(getClass().getClassLoader()));
} else {
final URL[] urls = this.extendedClassPathFiles.stream()
.filter(File::exists)
.filter(file -> file.isDirectory() || file.getName().endsWith(".jar"))
.flatMap(compose(compose(File::toURI, result1(URI::toURL)), Result::stream))
.toArray(URL[]::new);
return Result.success(new URLClassLoader(urls, opear.getPlanClassLoader(parentClassLoader)));
}
}

Result<Options> build(final @NotNull Console console) {
final File realCacheDir = this.cacheDir != null
? this.cacheDir
: console.getCwd().toPath().resolve(CACHE_DIR_NAME).toFile().getAbsoluteFile();
final File opearCache = new File(realCacheDir, "opears");
opearCache.mkdirs();

Result<Opear> opearResult = buildOpear(console, opearCache);

return opearResult.flatMap(opear -> Optional.ofNullable(planName).map(opear::getSpecificPlan)
.orElse(Result.success(noPlan ? OakpalPlan.EMPTY_PLAN_URL : opear.getDefaultPlan()))
.flatMap(planUrl -> getExtendedClassLoader(opear, getClass().getClassLoader())
.flatMap(classLoader -> messageWriter(console, outputJson, outFile).map(writer ->
new Options(justHelp, justVersion, storeBlobs, planUrl,
classLoader, realCacheDir, opearFile, planName, planFromFile,
planFromFileBaseDir, preInstallFiles, extendedClassPathFiles,
noHooks, scanFiles, writer, Optional.ofNullable(failOnSeverity)
.orElse(DEFAULT_OPTIONS.failOnSeverity))))));
}
}

/**
Expand Down
Loading

0 comments on commit a737437

Please sign in to comment.