Cli.java
package com.github.dakusui.symfonion.cli;
import com.github.dakusui.logias.lisp.Context;
import com.github.dakusui.symfonion.cli.subcommands.PresetSubcommand;
import com.github.dakusui.symfonion.core.Symfonion;
import com.github.dakusui.symfonion.exceptions.CliException;
import com.github.dakusui.symfonion.exceptions.SymfonionException;
import org.apache.commons.cli.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static com.github.dakusui.symfonion.cli.CliUtils.composeErrMsg;
import static java.lang.String.format;
public record Cli(Subcommand subcommand, File source, File sink, MidiRouteRequest routeRequest,
/*
* Returns a map that defines MIDI-in port names.
* A key in the returned map is a port name used in a symfonion song file.
* The value associated with it is a regular expression that should specify a MIDI device.
* The regular expression should be defined so that it matches one and only one MIDI-in device available in the system.
*/
Map<String, Pattern> midiInRegexPatterns,
/*
* Returns a map that defines MIDI-out port names.
* A key in the returned map is a port name used in a symfonion song file.
* The value associated with it is a regular expression that should specify a MIDI device.
* The regular expression should be defined so that it matches one and only one MIDI-out device available in the system.
*/
Map<String, Pattern> midiOutRegexPatterns,
Options options,
Symfonion symfonion) {
/**
* Returns an {@code Options} object which represents the specification of this CLI command.
*
* @return an {@code Options} object for this {@code CLI} class.
*/
static Options buildOptions() {
// create Options object
Options options = new Options();
// //
// Behavior options
options.addOption("V", "version", false, "print the version information.");
options.addOption("h", "help", false, "print the command line usage.");
options.addOption("l", "list", false, "list the available midi devices.");
options.addOption("p", "play", true, "play the specified file.");
options.addOption("c", "compile", true,
"compile the specified file to a standard midi file.");
{
Option option = OptionBuilder.create("r");
option.setLongOpt("route");
option.setValueSeparator('=');
option.setArgs(2);
option.setDescription("run a midi patch bay.");
options.addOption(option);
}
// //
// I/O options
{
Option option = OptionBuilder.create("O");
option.setValueSeparator('=');
option.setArgs(2);
option.setDescription("specify midi out port.");
options.addOption(option);
}
{
Option option = OptionBuilder.create("I");
option.setValueSeparator('=');
option.setArgs(2);
option.setDescription("specify midi in port.");
options.addOption(option);
}
{
Option option = OptionBuilder.create("o");
option.setArgs(1);
option.setDescription("specify a file to which a compiled standard midi file is output.");
options.addOption(option);
}
return options;
}
static Symfonion createSymfonion() {
return new Symfonion(Context.ROOT.createChild());
}
static CommandLine parseArgs(Options options, String[] args) throws ParseException {
CommandLineParser parser = new GnuParser();
return parser.parse(options, args);
}
static Map<String, Pattern> parseSpecifiedOptionsInCommandLineAsPortNamePatterns(CommandLine cmd, String optionName) throws CliException {
Properties props = cmd.getOptionProperties(optionName);
Map<String, Pattern> ret = new HashMap<>();
for (Object key : props.keySet()) {
String portName = key.toString();
String p = props.getProperty(portName);
try {
Pattern portpattern = Pattern.compile(p);
ret.put(portName, portpattern);
} catch (PatternSyntaxException e) {
throw new CliException(composeErrMsg(
format("Regular expression '%s' for '%s' isn't valid.", portName, p),
optionName,
null), e);
}
}
return ret;
}
public static int invoke(PrintStream stdout, PrintStream stderr, String... args) {
int ret;
try {
Cli cli = new Builder(args).build();
cli.subcommand().invoke(cli, stdout, System.in);
ret = 0;
} catch (ParseException e) {
printError(stderr, e);
ret = 1;
} catch (CliException e) {
printError(stderr, e);
ret = 2;
} catch (SymfonionException e) {
printError(stderr, e);
ret = 3;
} catch (IOException e) {
e.printStackTrace(stderr);
ret = 4;
} catch (Exception e) {
e.printStackTrace(stderr);
ret = 5;
}
return ret;
}
private static void printError(PrintStream ps, Throwable t) {
ps.printf("symfonion: %s%n", t.getMessage());
}
public static void main(String... args) {
if (args.length == 0 && !GraphicsEnvironment.isHeadless()) {
fallbackToSimpleGUI();
} else {
int exitCode = invoke(System.out, System.err, args);
System.exit(exitCode);
}
}
static void fallbackToSimpleGUI() {
String selectedFile = filenameFromFileChooser();
if (selectedFile != null) {
String[] args = new String[]{selectedFile};
final JTextArea textArea = new JTextArea();
JFrame frame = new JFrame("symfonion output");
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.add(textArea);
frame.pack();
frame.setSize(800, 600);
frame.setVisible(true);
PrintStream ps = new PrintStream(new OutputStream() {
private final StringBuilder sb = new StringBuilder();
@Override
public void flush() {
}
@Override
public void close() {
}
@Override
public void write(int b) {
if (b == '\r')
return;
if (b == '\n') {
final String text = sb + "\n";
SwingUtilities.invokeLater(() -> textArea.append(text));
sb.setLength(0);
return;
}
sb.append((char) b);
}
});
System.setOut(ps);
System.setErr(ps);
invoke(System.out, System.err, args);
}
}
static String filenameFromFileChooser() {
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
int result = chooser.showOpenDialog(new JFrame());
if (result == JFileChooser.APPROVE_OPTION) {
return chooser.getSelectedFile().getAbsolutePath();
}
return null;
}
public static class Builder {
private final String[] args;
private File source;
private File sink = new File("a.midi");
private MidiRouteRequest routeRequest = null;
private Map<String, Pattern> midiInRegexPatterns = new HashMap<>();
private Map<String, Pattern> midiOutRegexPatterns = new HashMap<>();
public Builder(String... args) {
this.args = args;
}
public Cli build() throws ParseException {
Options options1 = buildOptions();
CommandLine cmd = parseArgs(options1, args);
if (cmd.hasOption('O')) {
this.midiOutRegexPatterns = parseSpecifiedOptionsInCommandLineAsPortNamePatterns(cmd, "O");
}
if (cmd.hasOption('I')) {
this.midiInRegexPatterns = parseSpecifiedOptionsInCommandLineAsPortNamePatterns(cmd, "I");
}
if (cmd.hasOption('o')) {
String sinkFilename = CliUtils.getSingleOptionValueFromCommandLine(cmd, "o");
if (sinkFilename == null) {
throw new CliException(composeErrMsg("Output filename is required by this option.", "o"));
}
this.sink = new File(sinkFilename);
}
Subcommand subcommand;
if (cmd.hasOption("V") || cmd.hasOption("version")) {
subcommand = PresetSubcommand.VERSION;
} else if (cmd.hasOption("h") || cmd.hasOption("help")) {
subcommand = PresetSubcommand.HELP;
} else if (cmd.hasOption("l") || cmd.hasOption("list")) {
subcommand = PresetSubcommand.LIST;
} else if (cmd.hasOption("p") || cmd.hasOption("play")) {
subcommand = PresetSubcommand.PLAY;
String sourceFilename = CliUtils.getSingleOptionValueFromCommandLine(cmd, "p");
if (sourceFilename == null) {
throw new CliException(composeErrMsg("Input filename is required by this option.", "p"));
}
this.source = new File(sourceFilename);
} else if (cmd.hasOption("c") || cmd.hasOption("compile")) {
subcommand = PresetSubcommand.COMPILE;
String sourceFilename = CliUtils.getSingleOptionValueFromCommandLine(cmd, "c");
if (sourceFilename == null) {
throw new CliException(composeErrMsg("Input filename is required by this option.", "c"));
}
this.source = new File(sourceFilename);
} else if (cmd.hasOption("r") || cmd.hasOption("route")) {
subcommand = PresetSubcommand.ROUTE;
Properties props = cmd.getOptionProperties("r");
if (props.size() != 1) {
throw new CliException(composeErrMsg("Route information is not given or specified multiple times.", "r", "route"));
}
this.routeRequest = new MidiRouteRequest(cmd.getOptionValues('r')[0], cmd.getOptionValues('r')[1]);
} else {
@SuppressWarnings("unchecked")
List<String> leftovers = cmd.getArgList();
if (leftovers.isEmpty()) {
subcommand = PresetSubcommand.HELP;
} else if (leftovers.size() == 1) {
subcommand = PresetSubcommand.PLAY;
this.source = new File(leftovers.getFirst());
} else {
throw new CliException(composeErrMsg(format("Unrecognized arguments:%s", leftovers.subList(2, leftovers.size())), "-"));
}
}
return new Cli(subcommand, source, sink, routeRequest, midiInRegexPatterns, midiOutRegexPatterns, options1, createSymfonion());
}
}
}