package com.loopperfect.buckaroo.routines;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.*;
import com.google.common.hash.HashCode;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.loopperfect.buckaroo.*;
import com.loopperfect.buckaroo.crypto.Hash;
import com.loopperfect.buckaroo.io.IO;
import com.loopperfect.buckaroo.serialization.Serializers;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import static com.loopperfect.buckaroo.Either.left;
public final class Routines {
private Routines() {
}
public static IO<Optional<IOException>> upgrade(
final String buckarooDirectory, final RemoteCookBook cookBook) {
Preconditions.checkNotNull(buckarooDirectory);
Preconditions.checkNotNull(cookBook);
final String cookBookPath = buckarooDirectory + "/" + cookBook.name;
return ensureCheckout(cookBookPath, GitCommit.of(cookBook.url, "master"));
}
public static IO<Optional<IOException>> upgradeForConfig(final Path configFilePath) {
Preconditions.checkNotNull(configFilePath);
return IO.value(configFilePath)
.flatMap(x -> readConfig(x.toString()))
.flatMap(readConfigResult -> readConfigResult.join(
e -> IO.value(Optional.of(e)),
config -> buckarooDirectory.flatMap(path -> continueUntilPresent(
config.cookBooks.stream()
.map(cookBook -> IO.println("Upgrading " + cookBook.name + "...")
.then(upgrade(path, cookBook)))
.collect(ImmutableList.toImmutableList())))));
}
public static IO<Optional<IOException>> ensureConfig =
IO.of(context -> {
Preconditions.checkNotNull(context);
return context.fs().getPath(
context.fs().homeDirectory(),
"/",
".buckaroo",
"/",
"config.json");
}).flatMap(configFile -> IO.of(context -> {
Preconditions.checkNotNull(context);
try {
if (!context.fs().exists(configFile.toString())) {
final String defaultConfig = Resources.toString(
Resources.getResource("com.loopperfect.buckaroo/DefaultConfig.txt"),
Charsets.UTF_8);
context.fs().writeFile(configFile.toString(), defaultConfig);
return upgradeForConfig(configFile).run(context);
}
return Optional.empty();
} catch (final IOException e) {
return Optional.of(e);
}
}));
public static final IO<String> buckarooDirectory =
context -> Paths.get(context.fs().homeDirectory(), ".buckaroo/").toString();
public static final IO<String> configFilePath =
buckarooDirectory.map(x -> Paths.get(x, "config.json").toString());
public static final IO<String> projectFilePath =
context -> Paths.get(context.fs().workingDirectory(), "buckaroo.json").toString();
public static IO<Either<IOException, Project>> readProject(final String path) {
Preconditions.checkNotNull(path);
return context -> context.fs().readFile(path).join(
Either::left,
content -> Serializers.parseProject(content).leftProjection(IOException::new));
}
public static IO<Optional<IOException>> writeProject(
final String path, final Project project, final boolean overwrite) {
Preconditions.checkNotNull(path);
Preconditions.checkNotNull(project);
return IO.writeFile(path, Serializers.serialize(project), overwrite);
}
public static IO<Either<IOException, BuckarooConfig>> readConfig(final String path) {
Preconditions.checkNotNull(path);
return context -> context.fs().readFile(path).join(
Either::left,
content -> {
Preconditions.checkNotNull(content);
return Serializers.parseConfig(content).leftProjection(IOException::new);
});
}
private static IO<Either<IOException, Recipe>> readRecipe(final String path) {
Preconditions.checkNotNull(path);
return IO.of(x -> x.fs().readFile(path))
.map(x -> x.join(
Either::left,
content -> Serializers.parseRecipe(content).leftProjection(IOException::new)));
}
public static <L, R> IO<Either<L, ImmutableList<R>>> allOrNothing(final ImmutableList<IO<Either<L, R>>> xs) {
Preconditions.checkNotNull(xs);
return context -> {
Preconditions.checkNotNull(context);
final ImmutableList.Builder builder = ImmutableList.builder();
for (final IO<Either<L, R>> x : xs) {
final Either<L, R> result = x.run(context);
if (result.left().isPresent()) {
return left(result.left().get());
}
builder.add(result.right().get());
}
return Either.right(builder.build());
};
}
public static <T> IO<Optional<T>> continueUntilPresent(final ImmutableList<IO<Optional<T>>> xs) {
Preconditions.checkNotNull(xs);
return context -> {
Preconditions.checkNotNull(context);
for (final IO<Optional<T>> x : xs) {
final Optional<T> result = x.run(context);
if (result.isPresent()) {
return result;
}
}
return Optional.empty();
};
}
private static IO<Either<IOException, ImmutableList<Identifier>>> listRecipesForOrganization(final String path) {
Preconditions.checkNotNull(path);
return context -> {
Preconditions.checkNotNull(context);
return context.fs().listFiles(path)
.rightProjection(files -> files.stream()
.filter(file -> context.fs().isFile(file) &&
Files.getFileExtension(file).equalsIgnoreCase("json"))
.map(file -> context.fs().getPath(file).getFileName().toString())
.map(file -> file.substring(0, file.length() - ".json".length()))
.filter(Identifier::isValid)
.map(Identifier::of)
.distinct()
.collect(ImmutableList.toImmutableList()));
};
}
private static IO<Either<IOException, ImmutableList<Identifier>>> listOrganizationsForCookBook(final String cookBookPath) {
Preconditions.checkNotNull(cookBookPath);
return context -> {
Preconditions.checkNotNull(context);
return context.fs().listFiles(context.fs().getPath(cookBookPath, "recipes").toString())
.rightProjection(files -> files.stream()
.filter(file -> Files.getFileExtension(file).equalsIgnoreCase("json") &&
context.fs().isFile(file))
.map(file -> context.fs().getPath(file).getFileName().toString())
.map(file -> file.substring(0, file.length() - ".json".length()))
.filter(Identifier::isValid)
.map(Identifier::of)
.distinct()
.collect(ImmutableList.toImmutableList()));
};
}
public static IO<Either<IOException, Organization>> readOrganization(
final String path, final Identifier identifier) {
Preconditions.checkNotNull(path);
Preconditions.checkNotNull(identifier);
return listRecipesForOrganization(path + "/" + identifier.name + "/")
.flatMap(x -> x.join(
error -> IO.value(left(error)),
identifiers -> allOrNothing(
identifiers.stream()
.map(i -> readRecipe(path + "/" + identifier.name + "/" + i.name + ".json")
.map(y -> y.rightProjection(z -> Maps.immutableEntry(i, z)))
.map(y -> y.leftProjection(z ->
new IOException("Error reading recipe at " + path + "/" + identifier.name + "/" + i.name + ".json", z))))
.collect(ImmutableList.toImmutableList()))
.map(y -> y.rightProjection(
recipes -> Organization.of(identifier.name, recipes.stream()
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)))))));
}
private static IO<Either<IOException, CookBook>> readCookBook(final String path) {
Preconditions.checkNotNull(path);
return listOrganizationsForCookBook(path)
.flatMap(x -> x.join(
error -> IO.value(left(error)),
identifiers -> allOrNothing(
identifiers.stream()
.map(identifier -> readOrganization(path + "/recipes", identifier)
.map(i -> i.rightProjection(j -> Maps.immutableEntry(identifier, j))))
.collect(ImmutableList.toImmutableList()))
.map(y -> y.rightProjection(organizations -> CookBook.of(organizations.stream()
.collect(ImmutableMap.toImmutableMap(
Map.Entry::getKey,
Map.Entry::getValue)))))));
}
public static IO<Either<IOException, ImmutableList<CookBook>>> readCookBooks(final BuckarooConfig config) {
Preconditions.checkNotNull(config);
return allOrNothing(config.cookBooks.stream()
.map(remoteCookBook -> buckarooDirectory
.flatMap(path -> context -> context.fs()
.getPath(path, remoteCookBook.name.toString()).toString())
.flatMap(Routines::readCookBook)
.map(x -> x.leftProjection(y -> new IOException("Error reading " + remoteCookBook.name, y))))
.collect(ImmutableList.toImmutableList()));
}
public static IO<Optional<IOException>> fetchSource(final String path, final Either<GitCommit, RemoteArchive> source) {
Preconditions.checkNotNull(path);
Preconditions.checkNotNull(source);
return Either.join(
source,
gitCommit -> ensureCheckout(path, gitCommit),
remoteFile -> fetchAndUnzip(path, remoteFile));
}
/**
* Fetches a remote-file and downloads it to the given path.
* If a file is already present, then its hash is checked against what is expected.
*
* An error is returned if the process failed in any way, and a nothing otherwise.
*
* The process may fail in a number of ways:
*
* - There is already a directory at the target path
* - There is already a file at the target path and it has the wrong hash
* - It was not possible to download the file
* - The file could not be written to the target path
* - The hash of the downloaded file did not match the expected hash
*
* @return An error if the process failed in any way,
* and a nothing otherwise.
*/
public static IO<Optional<IOException>> fetchRemoteFile(final String path, final RemoteFile remoteFile) {
Preconditions.checkNotNull(path);
Preconditions.checkNotNull(remoteFile);
return context -> {
Preconditions.checkNotNull(context);
if (context.fs().exists(path)) {
if (context.fs().isDirectory(path)) {
return Optional.of(new IOException("There is a directory at " + path + "! "));
}
} else {
final Optional<IOException> download =
context.http().download(remoteFile.url, context.fs().getPath(path));
if (download.isPresent()) {
return download;
}
}
final Either<IOException, HashCode> actual = Hash.sha256(context.fs().getPath(path));
if (!Objects.equals(actual, Either.right(remoteFile.sha256))) {
return Optional.of(new IOException("Hash mismatch! Expected " + remoteFile.sha256 +
" but got " + actual.join(l -> "<invalid>", HashCode::toString) + ". "));
}
return Optional.empty();
};
}
public static IO<Optional<IOException>> fetchAndUnzip(final String path, final RemoteArchive remoteFile) {
Preconditions.checkNotNull(path);
Preconditions.checkNotNull(remoteFile);
return context -> {
Preconditions.checkNotNull(context);
final Optional<IOException> fetchResult = fetchRemoteFile(path + ".zip", remoteFile.asRemoteFile()).run(context);
if (fetchResult.isPresent()) {
return fetchResult;
}
final Path zipPath = context.fs().getPath(path + ".zip");
final Path targetPath = context.fs().getPath(path);
return com.loopperfect.buckaroo.io.Files.unzip(
zipPath, targetPath, remoteFile.subPath.map(x -> context.fs().getPath(x)));
};
}
public static IO<Optional<IOException>> ensureCheckout(final String path, final GitCommit gitCommit) {
Preconditions.checkNotNull(path);
Preconditions.checkNotNull(gitCommit);
return IO.of(context -> context.fs().getPath(path).toFile())
// Try to clone and pull, ignoring any errors...
.flatMap(file -> IO.sequence(ImmutableList.of(
context -> context.git().clone(file, gitCommit.url),
context -> context.git().pull(file)))
// Now if we succeeded, the checkout should not fail...
.then(context -> context.git().checkout(file, gitCommit.commit)
.map(e -> new IOException("Could not checkout " + gitCommit.encode() + " to " + path, e))));
}
/**
* Loads the Buckaroo config from the expected path.
* If there is no config available, the default config is created automatically.
*
* @return An error if the process failed in any way,
* and the Buckaroo config otherwise.
*/
public static IO<Either<IOException, BuckarooConfig>> loadConfig = ensureConfig
.flatMap(x -> Optionals.join(x,
i -> IO.value(left(i)),
() -> configFilePath.flatMap(Routines::readConfig)));
/**
* Gets a UUID for this user on this system.
* If the process fails, then a new UUID is generated.
*
* @return A random UUID tied to this system
*/
public static final IO<String> getIdentifier = IO.of(context -> {
Preconditions.checkNotNull(context);
final String path = context.fs().getPath(
Routines.buckarooDirectory.run(context),
"user-uuid.txt").toString();
if (!context.fs().exists(path)) {
final String identifier = UUID.randomUUID().toString();
context.fs().writeFile(path, identifier);
return identifier;
}
return context.fs().readFile(path).join(
e -> UUID.randomUUID().toString(),
x -> x.trim());
});
}