Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isolated updates #64

Merged
merged 9 commits into from
Aug 28, 2016
1 change: 1 addition & 0 deletions bin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mainClassName = 'io.github.alechenninger.monarch.Main'
dependencies {
compile 'org.yaml:snakeyaml:1.16'
compile 'net.sourceforge.argparse4j:argparse4j:0.7.0'
compile 'com.google.guava:guava:19.0'
compile project(':monarch-lib')
testCompile 'org.codehaus.groovy:groovy:2.4.5'
testCompile 'junit:junit:4.12'
Expand Down
50 changes: 30 additions & 20 deletions bin/src/io/github/alechenninger/monarch/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
Expand All @@ -42,8 +42,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
Expand All @@ -60,8 +58,6 @@ public class Main {

private final MonarchArgParser parser;

private static final Charset UTF_8 = Charset.forName("UTF-8");

public Main(Monarch monarch, Yaml yaml, String defaultConfigPath, FileSystem fileSystem,
MonarchParsers parsers, OutputStream consoleOut) {
this.monarch = monarch;
Expand Down Expand Up @@ -140,15 +136,15 @@ public int run(String... args) {
.orElseThrow(missingOptionException("data directory"));
Hierarchy hierarchy = options.hierarchy()
.orElseThrow(missingOptionException("hierarchy"));
SourceSpec target = options.target()
SourceSpec targetSpec = options.target()
.orElseThrow(missingOptionException("target"));

Map<String, Map<String, Object>> currentData =
Map<String, SourceData> currentData =
parsers.parseDataSourcesInHierarchy(dataDir, hierarchy);
Source source = hierarchy.sourceFor(target).orElseThrow(
() -> new IllegalArgumentException("Target source not found in hierarchy: " + target));
Source target = hierarchy.sourceFor(targetSpec).orElseThrow(
() -> new IllegalArgumentException("Target source not found in hierarchy: " + targetSpec));

applyChanges(outputDir, options.changes(), options.mergeKeys(), currentData, source);
applyChanges(outputDir, options.changes(), options.mergeKeys(), currentData, target);
} catch (Exception e) {
printError(e);
consoleOut.println();
Expand All @@ -161,34 +157,48 @@ public int run(String... args) {
}

private void applyChanges(Path outputDir, Iterable<Change> changes, Set<String> mergeKeys,
Map<String, Map<String, Object>> currentData, Source source) throws IOException {
Map<String, SourceData> currentSources, Source target) throws IOException {
if (!changes.iterator().hasNext()) {
consoleOut.println("No changes provided; formatting target.");
}

List<String> affectedSources = source.descendants().stream()
List<String> affectedSources = target.descendants().stream()
.map(Source::path)
.collect(Collectors.toList());
// TODO: Consider currentSources of type Sources or something like that with getter for this
Map<String, Map<String, Object>> currentData = currentSources.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().data()));

Map<String, Map<String, Object>> result = monarch.generateSources(
source, changes, currentData, mergeKeys);
target, changes, currentData, mergeKeys);

for (Map.Entry<String, Map<String, Object>> pathToData : result.entrySet()) {
String path = pathToData.getKey();

// We only output a source if it is target or under.
if (!affectedSources.contains(path)) {
continue;
}

Path sourcePath = outputDir.resolve(path);
ensureParentDirectories(sourcePath);
Path outPath = outputDir.resolve(path);
Map<String, Object> outData = pathToData.getValue();
SourceData sourceData = currentSources.containsKey(path)
? currentSources.get(path)
: parsers.forPath(outPath).newSourceData();

SortedMap<String, Object> sorted = new TreeMap<>(pathToData.getValue());
if (sourceData.isEmpty() && outData.isEmpty()) {
continue;
}

if (sorted.isEmpty()) {
Files.write(sourcePath, new byte[]{});
} else {
yaml.dump(sorted, Files.newBufferedWriter(sourcePath, UTF_8));
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
sourceData.writeNew(outData, out);
ensureParentDirectories(outPath);
Files.write(outPath, out.toByteArray());
} catch (Exception e) {
// TODO: Proper logger
new MonarchException("Failed to write updated data source at " + path + " to " + outPath, e)
.printStackTrace(consoleOut);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class MonarchFileParseException extends MonarchException {
private final Path path;

public MonarchFileParseException(String whatWasBeingParsed, Path path, Exception cause) {
super("Failed to parse '" + whatWasBeingParsed + "' at: " + path , cause);
super("Failed to parse " + whatWasBeingParsed + " at: " + path , cause);
this.path = path;
}

Expand Down
4 changes: 4 additions & 0 deletions bin/src/io/github/alechenninger/monarch/MonarchParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

package io.github.alechenninger.monarch;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

Expand All @@ -33,4 +35,6 @@ public interface MonarchParser {
Hierarchy parseHierarchy(InputStream hierarchyInput);
List<Change> parseChanges(InputStream changesInput);
Map<String, Object> parseMap(InputStream inputStream);
SourceData newSourceData();
SourceData parseData(InputStream inputStream) throws IOException;
}
51 changes: 43 additions & 8 deletions bin/src/io/github/alechenninger/monarch/MonarchParsers.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ default List<Change> parseChanges(String pathOrParseable, FileSystem fileSystem)
}
}

default Map<String, Object> parseData(String pathOrParseable, FileSystem fileSystem) {
default Map<String, Object> parseMap(String pathOrParseable, FileSystem fileSystem) {
try {
Path path = fileSystem.getPath(pathOrParseable);
return parseData(path);
return parseMap(path);
} catch (InvalidPathException | MonarchFileParseException e) {
byte[] parseable = pathOrParseable.getBytes(Charset.forName("UTF-8"));
ByteArrayInputStream parseableStream = new ByteArrayInputStream(parseable);
Expand All @@ -133,29 +133,64 @@ default Map<String, Object> parseData(String pathOrParseable, FileSystem fileSys
return yaml().parseMap(parseableStream);
} catch (Exception parseException) {
e.addSuppressed(parseException);
throw new MonarchException("Failed to parse data", e);
throw new MonarchException("Failed to parse map", e);
}
}
}

default Map<String, Object> parseData(Path path) {
default Map<String, Object> parseMap(Path path) {
try {
return forPath(path).parseMap(Files.newInputStream(path));
} catch (NoSuchFileException ignored) {
} catch (NoSuchFileException e) {
return Collections.emptyMap();
} catch (Exception e) {
throw new MonarchFileParseException("map", path, e);
}
}

/**
* If {@code pathOrParseable} is a valid file path but the file does not exist, an empty
* {@link SourceData} will be returned.
*/
default SourceData parseData(String pathOrParseable, FileSystem fileSystem) {
try {
Path path = fileSystem.getPath(pathOrParseable);
return parseData(path);
} catch (InvalidPathException | MonarchFileParseException e) {
byte[] parseable = pathOrParseable.getBytes(Charset.forName("UTF-8"));
ByteArrayInputStream parseableStream = new ByteArrayInputStream(parseable);

try {
return yaml().parseData(parseableStream);
} catch (Exception parseException) {
e.addSuppressed(parseException);
throw new MonarchException("Failed to parse data", e);
}
}
}

/**
* If {@code path} does not exist, an empty {@link SourceData} will be returned.
*/
default SourceData parseData(Path path) {
try {
return forPath(path).parseData(Files.newInputStream(path));
} catch (NoSuchFileException e) {
return forPath(path).newSourceData();
} catch (Exception e) {
throw new MonarchFileParseException("data", path, e);
}
}

default Map<String, Map<String, Object>> parseDataSourcesInHierarchy(Path dataDir, Hierarchy hierarchy) {
Map<String, Map<String, Object>> data = new HashMap<>();
default Map<String, SourceData> parseDataSourcesInHierarchy(Path dataDir, Hierarchy hierarchy) {
Map<String, SourceData> data = new HashMap<>();

hierarchy.descendants().stream()
// TODO .parallel() but yaml is not threadsafe
.map(Source::path)
.forEach(source -> {
Path sourcePath = dataDir.resolve(source);
Map<String, Object> sourceData = parseData(sourcePath);
SourceData sourceData = parseData(sourcePath);
data.put(source, sourceData);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@

package io.github.alechenninger.monarch;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
Expand Down
46 changes: 46 additions & 0 deletions bin/src/io/github/alechenninger/monarch/SourceData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* monarch - A tool for managing hierarchical data.
* Copyright (C) 2016 Alec Henninger
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package io.github.alechenninger.monarch;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

public interface SourceData {

Map<String, Object> data();

default boolean isEmpty() {
return data().isEmpty();
}

default boolean isNotEmpty() {
return !isEmpty();
}

/**
* Outputs {@code newData} to {@code out} in the same data format as the source.
*
* <p>Some implementations may use the original source to influence the output, such as by
* maintaining comments, formatting, or style, etc.
*
* <p>Closes {@code out} when done.
*/
void writeNew(Map<String, Object> newData, OutputStream out) throws IOException;
}
Loading