Skip to content

Commit

Permalink
Application contrib maintenance (#3108)
Browse files Browse the repository at this point in the history
* add second run method for convenience

* improve config update from yaml and made it public

* implement methods to run automatically from gui

* fixes for run from gui

* move command classes to enable usage of config path

* reorder if statements

* fix merging of default arguments

* support list of doubles and ints as parameters

* fix merge config if there is only one param set

* add heuristics to detect if run from desktop on windows

* fix detection of powershell

* add checks if run from intelij
  • Loading branch information
rakow authored Feb 13, 2024
1 parent b087d76 commit f403824
Show file tree
Hide file tree
Showing 11 changed files with 557 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package org.matsim.application;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.api.core.v01.Scenario;
import org.matsim.application.options.CrsOptions;
import org.matsim.application.options.InputOptions;
import org.matsim.application.options.OutputOptions;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigAliases;
import org.matsim.core.config.ConfigGroup;
import org.matsim.core.config.ConfigUtils;
import org.matsim.core.scenario.ScenarioUtils;
import org.matsim.core.utils.io.IOUtils;
import picocli.CommandLine;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
Expand All @@ -21,13 +28,20 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class ApplicationUtils {

private static final Logger log = LogManager.getLogger(ApplicationUtils.class);

/**
* This encoding indicates command line was used to start the jar.
*/
private static final String WIN_CLI_ENCODING = "cp850";

private ApplicationUtils() {
}

Expand All @@ -46,7 +60,187 @@ public static String[] mergeArgs(String[] args, String... defaultArgs) {
}

/**
* Extends a context (usually config location) with an relative filename.
* Utility method to check if a jar might be run from the desktop (using double-click).
*/
public static boolean isRunFromDesktop() {

// check if gui was explicitly enabled
String env = System.getenv().getOrDefault("RUN_GUI", "undefined");
if (env.equalsIgnoreCase("true") || env.equals("1"))
return true;
else if (env.equalsIgnoreCase("false") || env.equals("0"))
return false;

String property = System.getProperty("RUN_GUI", "undefined");
if (property.equalsIgnoreCase("true") || property.equals("1"))
return true;
else if (property.equalsIgnoreCase("false") || property.equals("0"))
return false;

String macIdentifier = System.getenv().getOrDefault("__CFBundleIdentifier", "none");

if (macIdentifier.equals("com.apple.java.JarLauncher") || macIdentifier.equals("com.apple.JavaLauncher"))
return true;

String os = System.getProperty("os.name");

if (os.toLowerCase().startsWith("windows")) {

// presence of the prompt variable indicates that the jar was run from the command line
boolean hasPrompt = System.getenv().containsKey("PROMPT");

// this prompt is not set in PowerShell, so we need another check
if (hasPrompt)
return false;

// stdout.encoding from CLI are usually cp850
String encoding = System.getProperty("stdout.encoding", "none");
String sunEncoding = System.getProperty("sun.stdout.encoding", "none");

if (encoding.equals(WIN_CLI_ENCODING) || sunEncoding.equals(WIN_CLI_ENCODING))
return false;

// Run from intelij, will not start the gui by default
if (System.getenv().containsKey("IDEA_INITIAL_DIRECTORY"))
return false;
// also file.encoding=UTF-8, seems to be set by default in IntelliJ

// if no other cli indicators are present, we have to assume that the jar was run from the desktop
return true;
}

return false;

}

/**
* Apply run configuration in yaml format.
*/
public static void applyConfigUpdate(Config config, Path yaml) {

if (!Files.exists(yaml)) {
throw new IllegalArgumentException("Desired run config does not exist:" + yaml);
}

ObjectMapper mapper = new ObjectMapper(new YAMLFactory()
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES));

ConfigAliases aliases = new ConfigAliases();
Deque<String> emptyStack = new ArrayDeque<>();

try (BufferedReader reader = Files.newBufferedReader(yaml)) {

JsonNode node = mapper.readTree(reader);

Iterator<Map.Entry<String, JsonNode>> fields = node.fields();

while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String configGroupName = aliases.resolveAlias(field.getKey(), emptyStack);
ConfigGroup group = config.getModules().get(configGroupName);
if (group == null) {
log.warn("Config group not found: {}", configGroupName);
continue;
}

applyNodeToConfigGroup(field.getValue(), group);
}

} catch (IOException e) {
throw new UncheckedIOException(e);
}

}

/**
* Sets the json config into
*/
private static void applyNodeToConfigGroup(JsonNode node, ConfigGroup group) {

Iterator<Map.Entry<String, JsonNode>> fields = node.fields();

while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();

if (isParameterSet(field.getValue())) {

// store the current parameters sets, newly added sets are not merged with each other
List<? extends ConfigGroup> params = new ArrayList<>(group.getParameterSets(field.getKey()));

for (JsonNode item : field.getValue()) {
applyNodeAsParameterSet(field.getKey(), item, group, params);
}
} else {

if (field.getValue().isTextual())
group.addParam(field.getKey(), field.getValue().textValue());
else if (field.getValue().isArray()) {
// arrays are joined using ","
Stream<JsonNode> stream = StreamSupport.stream(field.getValue().spliterator(), false);
String string = stream.map(n -> n.isTextual() ? n.textValue() : n.toString()).collect(Collectors.joining(","));
group.addParam(field.getKey(), string);
} else
group.addParam(field.getKey(), field.getValue().toString());
}
}
}

/**
* Any array of complex object can be considered a config group.
*/
private static boolean isParameterSet(JsonNode node) {

if (!node.isArray())
return false;

// any object can be assigned as parameter set
for (JsonNode el : node) {
if (!el.isObject())
return false;
}

return true;
}

/**
* Handle possible update and creation of parameter sets within a config group.
*/
private static void applyNodeAsParameterSet(String groupName, JsonNode item, ConfigGroup group, List<? extends ConfigGroup> params) {

Iterator<Map.Entry<String, JsonNode>> it = item.fields();

// There was at least one matching group
boolean matched = false;

while (!params.isEmpty() && it.hasNext()) {

Map.Entry<String, JsonNode> attr = it.next();
List<? extends ConfigGroup> candidates = params.stream()
.filter(p -> p.getParams().containsKey(attr.getKey()))
.filter(p -> p.getParams().get(attr.getKey())
.equals(attr.getValue().isTextual() ? attr.getValue().textValue() : attr.getValue().toString()))
.toList();

if (candidates.isEmpty())
break;

matched = true;
params = candidates;
}

if (params.size() > 1) {
throw new IllegalArgumentException("Ambiguous parameter set: " + item);
} else if (params.size() == 1 && matched) {
applyNodeToConfigGroup(item, params.get(0));
} else {
ConfigGroup newGroup = group.createParameterSet(groupName);
group.addParameterSet(newGroup);
applyNodeToConfigGroup(item, newGroup);
}
}

/**
* Extends a context (usually config location) with a relative filename.
* If the results is a local file, the path will be returned. Otherwise, it will be an url.
* The results can be used as input for command line parameter or {@link IOUtils#resolveFileOrResource(String)}.
*
Expand Down Expand Up @@ -75,10 +269,11 @@ public static Path globFile(Path path, String pattern) {
PathMatcher m = path.getFileSystem().getPathMatcher("glob:" + pattern);

try {
return Files.list(path)
.filter(p -> m.matches(p.getFileName()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No " + pattern + " file found."));
try (Stream<Path> list = Files.list(path)) {
return list.filter(p -> m.matches(p.getFileName()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No " + pattern + " file found."));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down
Loading

0 comments on commit f403824

Please sign in to comment.