cdc.gv.tools.GvToAny Maven / Gradle / Ivy
package cdc.gv.tools;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.io.IoBuilder;
import cdc.gv.tools.GvToAny.MainArgs.Feature;
import cdc.util.cli.AbstractMainSupport;
import cdc.util.cli.FeatureMask;
import cdc.util.cli.OptionEnum;
import cdc.util.files.Files;
import cdc.util.files.SearchPath;
import cdc.util.lang.Piper;
import cdc.util.time.Chronometer;
/**
* Program used to convert a Graphviz file to different image formats, using one
* of the available layout engines.
*
* @author Damien Carbonne
*
*/
public final class GvToAny {
protected static final Logger LOGGER = LogManager.getLogger(GvToAny.class);
private static final PrintStream OUT = IoBuilder.forLogger(LOGGER).setLevel(Level.INFO).buildPrintStream();
private final MainArgs margs;
public static class MainArgs {
/** Name of the input Graphviz file. */
public File input;
public MainArgs setInput(File input) {
this.input = input;
return this;
}
/** Name of the output directory. */
public File outputDir;
public MainArgs setOutputDir(File outputDir) {
this.outputDir = outputDir;
return this;
}
/** Paths where Graphviz layout engines can be found. */
public SearchPath paths = new SearchPath();
/**
* The timeout in milliseconds before killing GraphViz.
*
* A negative value has no effect.
*/
public long timeout = -1L;
/** Engine to use for layout. */
public GvEngine engine;
public MainArgs setEngine(GvEngine engine) {
this.engine = engine;
return this;
}
/** Extra arguments to pass to the layout engine. */
public final List args = new ArrayList<>();
public MainArgs addArgs(String... args) {
for (final String arg : args) {
this.args.add(arg);
}
return this;
}
/**
* Extra arguments to pass to gvpack.
*
* Related to use of {@link Feature#PACK}
*/
public final List gvpackArgs = new ArrayList<>();
public MainArgs addGvpackArgs(String... args) {
for (final String arg : args) {
this.gvpackArgs.add(arg);
}
return this;
}
/**
* Extra arguments to pass to ccomps.
*
* Related to use of {@link Feature#PACK}
*/
public final List ccompsArgs = new ArrayList<>();
public MainArgs addCcompsArgs(String... args) {
for (final String arg : args) {
this.ccompsArgs.add(arg);
}
return this;
}
/** Output formats to use. */
public final Set formats = EnumSet.noneOf(GvFormat.class);
public MainArgs addFormat(GvFormat format) {
this.formats.add(format);
return this;
}
/** Enabling of features. */
protected final FeatureMask features = new FeatureMask<>();
/**
* Enumeration of possible boolean options.
*/
public enum Feature implements OptionEnum {
PACK("pack", "Split graph in subgraphs of connected components (using ccomps) and uses gvpack."),
VERBOSE("verbose", "Enable verbose mode. Add '-v' to command args to make Graphviz engine verbose."),
INVOKE_IF_NEWER("invoke-if-newer", "Invoke engine if target files don't exist or are older than input file.");
private final String name;
private final String description;
private Feature(String name,
String description) {
this.name = name;
this.description = description;
}
@Override
public final String getName() {
return name;
}
@Override
public final String getDescription() {
return description;
}
}
public MainArgs setEnabled(Feature feature,
boolean enabled) {
features.setEnabled(feature, enabled);
return this;
}
public boolean isEnabled(Feature feature) {
return features.isEnabled(feature);
}
}
private GvToAny(MainArgs margs) {
this.margs = margs;
}
private boolean appendFormats(List command,
String basename) {
// True if conversion should be invoked
// If INVOKE_IF_NEWER is disabled, always invoke
// Otherwise, we must check
boolean invoke = !margs.isEnabled(Feature.INVOKE_IF_NEWER);
for (final GvFormat format : margs.formats) {
command.add("-T" + format.getFormatName());
final File target = new File(margs.outputDir, basename + "." + format.getFormatName());
command.add("-o" + target.getPath());
if (!invoke && (!target.exists() || Files.isNewerThan(margs.input, target))) {
invoke = true;
}
}
return invoke;
}
private void execute() {
final String basename = margs.input.getName().replace(".gv", "");
final File exec = margs.paths.resolveExe(margs.engine.getEngineName());
final List> commands = new ArrayList<>();
final boolean invoke;
if (margs.isEnabled(Feature.PACK)) {
final File ccomps = margs.paths.resolveExe("ccomps");
final File gvpack = margs.paths.resolveExe("gvpack");
final File neato = margs.paths.resolveExe("neato");
// ccomps
final List cmd1 = new ArrayList<>();
cmd1.add(ccomps.getPath());
// cmd1.add("-x");
cmd1.add(margs.input.getPath());
for (final String arg : margs.ccompsArgs) {
cmd1.add(arg);
}
commands.add(cmd1);
// engine
final List cmd2 = new ArrayList<>();
cmd2.add(exec.getPath());
for (final String arg : margs.args) {
cmd2.add(arg);
}
commands.add(cmd2);
// gvpack
final List cmd3 = new ArrayList<>();
cmd3.add(gvpack.getPath());
for (final String arg : margs.gvpackArgs) {
cmd3.add(arg);
}
commands.add(cmd3);
// neato
final List cmd4 = new ArrayList<>();
cmd4.add(neato.getPath());
cmd4.add("-s");
cmd4.add("-n2");
invoke = appendFormats(cmd4, basename);
commands.add(cmd4);
} else {
// engine
final List cmd1 = new ArrayList<>();
cmd1.add(exec.getPath());
cmd1.add(margs.input.getPath());
for (final String arg : margs.args) {
cmd1.add(arg);
}
invoke = appendFormats(cmd1, basename);
commands.add(cmd1);
}
if (invoke) {
final boolean verbose = margs.isEnabled(MainArgs.Feature.VERBOSE);
if (verbose) {
final StringBuilder builder = new StringBuilder();
boolean first = true;
for (final List command : commands) {
if (first) {
first = false;
} else {
builder.append(" | ");
}
builder.append(command);
}
LOGGER.info(builder);
}
final Chronometer chrono = new Chronometer();
chrono.start();
final OutputStream os = new ByteArrayOutputStream();
try (final InputStream is = Piper.pipeSplitList(margs.timeout, commands)) {
int b;
while ((b = is.read()) != -1) {
os.write(b);
if (verbose) {
OUT.write(b);
}
}
chrono.suspend();
if (verbose) {
LOGGER.info("Done in {}", chrono);
}
} catch (final IOException | InterruptedException e) {
LOGGER.error("execute() failed with commands: {} {}", commands, e.getMessage());
LOGGER.error(os.toString());
}
}
}
public static void execute(MainArgs margs) {
final GvToAny main = new GvToAny(margs);
main.execute();
}
public static void main(String[] args) {
final MainSupport support = new MainSupport();
support.main(args);
}
private static class MainSupport extends AbstractMainSupport {
private static final String ARG = "arg";
private static final String CCOMPS_ARG = "ccomps-arg";
private static final String GVPACK_ARG = "gvpack-arg";
private static final String TIMEOUT = "timeout";
public MainSupport() {
super(GvToAny.class, LOGGER);
}
@Override
protected String getVersion() {
return Config.VERSION;
}
@Override
protected void addSpecificOptions(Options options) {
options.addOption(Option.builder("i")
.longOpt(INPUT)
.desc("Gv input FILE/URL")
.hasArg()
.required()
.build());
options.addOption(Option.builder("o")
.longOpt(OUTPUT)
.desc("Output directory")
.hasArg()
.required()
.build());
options.addOption(Option.builder()
.longOpt(ARG)
.desc("Optional arguments to be passed to Graphviz layout engine.")
.hasArgs()
.build());
options.addOption(Option.builder()
.longOpt(CCOMPS_ARG)
.desc("Optional arguments to be passed to ccomps. Useful when PACK option is enabled.")
.hasArgs()
.build());
options.addOption(Option.builder()
.longOpt(GVPACK_ARG)
.desc("Optional arguments to be passed to gvpack. Useful when PACK option is enabled.")
.hasArgs()
.build());
options.addOption(Option.builder()
.longOpt(PATH)
.desc("Directory(ies) where the Graphviz binaries can be found.")
.hasArgs()
.build());
options.addOption(Option.builder()
.longOpt(TIMEOUT)
.desc("Optional timeout, in milliseconds, to wait before killing GraphViz.")
.hasArg()
.build());
addNoArgOptions(options, MainArgs.Feature.class);
addNoArgOptions(options, GvEngine.class);
addNoArgOptions(options, GvFormat.class);
}
@Override
protected MainArgs analyze(CommandLine cl) throws ParseException {
final MainArgs margs = new MainArgs();
margs.input = new File(cl.getOptionValue(INPUT));
if (!margs.input.isFile()) {
throw new ParseException("Invalid input: " + cl.getOptionValue('i'));
}
margs.outputDir = new File(cl.getOptionValue(OUTPUT));
margs.timeout = getValueAsLong(cl, TIMEOUT, -1L);
if (cl.hasOption(PATH)) {
margs.paths = new SearchPath(cl.getOptionValues(PATH));
}
for (final GvEngine engine : GvEngine.values()) {
if (cl.hasOption(engine.getName())) {
margs.engine = engine;
}
}
if (margs.engine == null) {
margs.engine = GvEngine.DOT;
}
if (cl.hasOption(ARG)) {
for (final String arg : cl.getOptionValues(ARG)) {
margs.args.add(arg);
}
}
if (cl.hasOption(CCOMPS_ARG)) {
for (final String arg : cl.getOptionValues(CCOMPS_ARG)) {
margs.ccompsArgs.add(arg);
}
}
if (cl.hasOption(GVPACK_ARG)) {
for (final String arg : cl.getOptionValues(GVPACK_ARG)) {
margs.gvpackArgs.add(arg);
}
}
for (final GvFormat format : GvFormat.values()) {
if (cl.hasOption(format.getName())) {
margs.formats.add(format);
}
}
if (margs.formats.isEmpty()) {
throw new ParseException("No output format");
}
setMask(cl, MainArgs.Feature.class, margs.features::setEnabled);
return margs;
}
@Override
protected Void execute(MainArgs margs) {
GvToAny.execute(margs);
return null;
}
}
}