com.github.dakusui.symfonion.CLI.java Source code

Java tutorial

Introduction

Here is the source code for com.github.dakusui.symfonion.CLI.java

Source

package com.github.dakusui.symfonion;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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 javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Transmitter;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import com.github.dakusui.logias.lisp.Context;
import com.github.dakusui.symfonion.core.SymfonionException;
import com.github.dakusui.symfonion.song.Keyword;
import com.github.dakusui.symfonion.song.Song;

public class CLI {
    static enum Mode {
        VERSION {
            @Override
            public void invoke(CLI cli, PrintStream ps) {
                ps.println("SyMFONION " + cli.version());
                ps.println(cli.license());
            }
        },
        HELP {
            @Override
            public void invoke(CLI cli, PrintStream ps) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printHelp("SYNTAX", cli.getOptions());
            }
        },
        LIST {
            @Override
            public void invoke(CLI cli, PrintStream ps) {
                MidiDeviceScanner.listAllDevices(System.out).scan();
            }
        },
        PLAY {
            @Override
            public void invoke(CLI cli, PrintStream ps) throws SymfonionException, IOException {
                Symfonion symfonion = cli.getSymfonion();

                Song song = symfonion.load(cli.getSourceFile().getAbsolutePath());
                Map<String, Sequence> sequences = symfonion.compile(song);
                ps.println();
                Map<String, MidiDevice> devices = cli.prepareMidiOutDevices(ps);
                ps.println();
                symfonion.play(devices, sequences);
            }
        },
        COMPILE {
            @Override
            public void invoke(CLI cli, PrintStream ps) throws SymfonionException, IOException {
                Symfonion symfonion = cli.getSymfonion();
                Song song = symfonion.load(cli.getSourceFile().getAbsolutePath());
                Map<String, Sequence> sequences = symfonion.compile(song);

                for (String portName : sequences.keySet()) {
                    Sequence seq = sequences.get(portName);
                    String outfile = cli.getSinkFile().getAbsolutePath();
                    File outputFile = cli.composeOutputFile(outfile, portName);
                    MidiSystem.write(seq, 1, outputFile);
                }
            }
        },
        ROUTE {
            @Override
            public void invoke(CLI cli, PrintStream ps) throws CLIException {
                Route route = cli.getRoute();
                String inPortName = route.in;
                String outPortName = route.out;

                Map<String, Pattern> inDefs = cli.getMidiInDefinitions();

                if (!inDefs.containsKey(inPortName)) {
                    String msg = cli.composeErrMsg(
                            String.format("MIDI-in port '%s' is specified, but it is not defined by '-I' option.",
                                    inPortName),
                            "r", "--route");
                    throw new CLIException(msg);
                }
                Pattern inRegex = inDefs.get(inPortName);
                MidiDeviceScanner inScanner = MidiDeviceScanner.chooseInputDevices(System.out, inRegex);
                inScanner.scan();
                MidiDevice.Info[] matchedInDevices = inScanner.getMatchedDevices();
                if (matchedInDevices.length == 0 || matchedInDevices.length > 1) {
                    String msg = cli.composeErrMsg(
                            String.format("MIDI-in device for %s(%s) is not found or found more than one.",
                                    inPortName, inRegex),
                            "I", null);
                    throw new CLIException(msg);
                }

                ps.println();

                Map<String, Pattern> outDefs = cli.getMidiOutDefinitions();
                if (!outDefs.containsKey(outPortName)) {
                    String msg = cli.composeErrMsg(
                            String.format("MIDI-out port '%s' is specified, but it is not defined by '-O' option.",
                                    inPortName),
                            "r", "route");
                    throw new CLIException(msg);
                }
                Pattern outRegex = outDefs.get(outPortName);
                MidiDeviceScanner outScanner = MidiDeviceScanner.chooseOutputDevices(System.out, outRegex);
                outScanner.scan();
                MidiDevice.Info[] matchedOutDevices = outScanner.getMatchedDevices();
                if (matchedOutDevices.length == 0 || matchedOutDevices.length > 1) {
                    String msg = cli.composeErrMsg(
                            String.format("MIDI-out device for %s(%s) is not found or found more than one.",
                                    outPortName, outRegex),
                            "I", null);
                    throw new CLIException(msg);
                }
                ps.println();
                patchbay(matchedInDevices[0], matchedOutDevices[0]);
            }

            void patchbay(MidiDevice.Info in, MidiDevice.Info out) throws CLIException {
                MidiDevice midiout;
                try {
                    midiout = MidiSystem.getMidiDevice(out);
                    midiout.open();
                } catch (MidiUnavailableException e) {
                    throw new CLIException(String.format("(-) Failed to open MIDI-out device (%s)", out.getName()),
                            e);
                }
                try {
                    MidiDevice midiin;
                    try {
                        midiin = MidiSystem.getMidiDevice(in);
                        midiin.open();
                    } catch (MidiUnavailableException ee) {
                        throw new CLIException(
                                String.format("(-) Failed to open MIDI-in device (%s)", in.getName()), ee);
                    }
                    try {
                        Receiver r = midiout.getReceiver();
                        try {
                            Transmitter t = midiin.getTransmitter();
                            try {
                                t.setReceiver(r);
                                System.out.println("Now in MIDI patch-bay mode. Hit enter to quit.");
                                System.in.read();
                            } catch (IOException e) {
                                System.out.println("quitting due to an error.");
                            } finally {
                                System.out.println("closing transmitter");
                                t.close();
                            }
                        } catch (MidiUnavailableException e) {
                            throw new CLIException(String.format(
                                    "(-) Failed to get transmitter from MIDI-in device (%s)", in.getName()), e);
                        } finally {
                            System.out.println("closing receiver");
                            r.close();
                        }
                    } catch (MidiUnavailableException e) {
                        throw new CLIException(String.format("(-) Failed to get receiver from MIDI-out device (%s)",
                                out.getName()), e);
                    } finally {
                        midiin.close();
                    }
                } finally {
                    midiout.close();
                }
            }
        };

        public abstract void invoke(CLI cli, PrintStream ps) throws SymfonionException, IOException;
    }

    static class Route {
        String in;
        String out;

        Route(String in, String out) {
            this.in = in;
            this.out = out;
        }
    }

    private Mode mode = Mode.VERSION;
    private File source;
    private File sink = new File("a.midi");
    private Route route = null;
    private Map<String, Pattern> midiins = new HashMap<String, Pattern>();
    private Map<String, Pattern> midiouts = new HashMap<String, Pattern>();
    private Symfonion symfonion;
    private Options options;

    public CLI(String... args) throws ParseException, CLIException {
        this.init(args);
        this.symfonion = createSymfonion();
    }

    protected Map<String, MidiDevice> prepareMidiOutDevices(PrintStream ps) throws CLIException {
        Map<String, Pattern> portDefinitions = this.getMidiOutDefinitions();
        String deviceType = "out";
        Map<String, MidiDevice> devices = prepareMidiDevices(ps, deviceType, portDefinitions);
        return devices;
    }

    protected Map<String, MidiDevice> prepareMidiInDevices(PrintStream ps) throws CLIException {
        Map<String, Pattern> portDefinitions = this.getMidiOutDefinitions();
        String deviceType = "in";
        Map<String, MidiDevice> devices = prepareMidiDevices(ps, deviceType, portDefinitions);
        return devices;
    }

    private Map<String, MidiDevice> prepareMidiDevices(PrintStream ps, String deviceType,
            Map<String, Pattern> portDefinitions) throws CLIException {
        Map<String, MidiDevice> devices = new HashMap<String, MidiDevice>();
        for (String portname : portDefinitions.keySet()) {
            Pattern regex = portDefinitions.get(portname);
            MidiDeviceScanner scanner = MidiDeviceScanner.chooseOutputDevices(ps, regex);
            scanner.scan();
            MidiDevice.Info[] matchedInfos = scanner.getMatchedDevices();
            if (matchedInfos.length > 1) {
                String msg = String.format("");
                throw new CLIException(msg);
            } else if (matchedInfos.length == 0) {
                String msg = String.format("");
                throw new CLIException(msg);
            }
            try {
                devices.put(portname, MidiSystem.getMidiDevice(matchedInfos[0]));
            } catch (MidiUnavailableException e) {
                String msg = this.composeErrMsg(String.format("Failed to access MIDI-%s device:'%s'.", deviceType,
                        matchedInfos[0].getName()), "O", null);
                throw new CLIException(msg, e);
            }
        }
        return devices;
    }

    protected File composeOutputFile(String outfile, String portName) {
        if (portName == null || Keyword.$default.equals(portName)) {
            return new File(outfile);
        }
        File ret = null;
        int lastIndexOfDot = outfile.lastIndexOf('.');
        if (lastIndexOfDot == -1) {
            ret = new File(outfile + "." + portName);
        } else {
            ret = new File(
                    outfile.substring(0, lastIndexOfDot) + "." + portName + outfile.substring(lastIndexOfDot));
        }
        return ret;
    }

    protected Options getOptions() {
        return this.options;
    }

    public Symfonion getSymfonion() {
        return this.symfonion;
    }

    public void init(String... args) throws ParseException, CLIException {
        this.options = buildOptions();
        this.analyze(parseArgs(this.options, args));
    }

    protected Symfonion createSymfonion() {
        return new Symfonion(Context.ROOT);
    }

    CommandLine parseArgs(Options options, String[] args) throws ParseException {
        CommandLineParser parser = new GnuParser();
        CommandLine ret = parser.parse(options, args);

        return ret;
    }

    private 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 specifiled 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;
    }

    public void analyze(CommandLine cmd) throws CLIException {
        if (cmd.hasOption('O')) {
            String optionName = "O";
            this.midiouts = initializeMidiPorts(cmd, optionName);
        }
        if (cmd.hasOption('I')) {
            String optionName = "I";
            this.midiins = initializeMidiPorts(cmd, optionName);
        }
        if (cmd.hasOption('o')) {
            String sinkFilename = getSingleOptionValue(cmd, "o");
            if (sinkFilename == null) {
                String msg = composeErrMsg("Output filename is required by this option.", "o", null);
                throw new CLIException(msg);
            }
            this.sink = new File(sinkFilename);
        }
        if (cmd.hasOption("V") || cmd.hasOption("version")) {
            this.mode = Mode.VERSION;
        } else if (cmd.hasOption("h") || cmd.hasOption("help")) {
            this.mode = Mode.HELP;
        } else if (cmd.hasOption("l") || cmd.hasOption("list")) {
            this.mode = Mode.LIST;
        } else if (cmd.hasOption("p") || cmd.hasOption("play")) {
            this.mode = Mode.PLAY;
            String sourceFilename = getSingleOptionValue(cmd, "p");
            if (sourceFilename == null) {
                String msg = composeErrMsg("Input filename is required by this option.", "p", null);
                throw new CLIException(msg);
            }
            this.source = new File(sourceFilename);
        } else if (cmd.hasOption("c") || cmd.hasOption("compile")) {
            this.mode = Mode.COMPILE;
            String sourceFilename = getSingleOptionValue(cmd, "c");
            if (sourceFilename == null) {
                String msg = composeErrMsg("Input filename is required by this option.", "c", null);
                throw new CLIException(msg);
            }
            this.source = new File(sourceFilename);
        } else if (cmd.hasOption("r") || cmd.hasOption("route")) {
            this.mode = Mode.ROUTE;
            Properties props = cmd.getOptionProperties("r");
            if (props.size() != 1) {
                String msg = composeErrMsg("Route information is not given or specified multiple times.", "r",
                        "route");
                throw new CLIException(msg);
            }

            this.route = new Route(cmd.getOptionValues('r')[0], cmd.getOptionValues('r')[1]);
        } else {
            @SuppressWarnings("unchecked")
            List<String> leftovers = cmd.getArgList();
            if (leftovers.size() == 0) {
                this.mode = Mode.HELP;
            } else if (leftovers.size() == 1) {
                this.mode = Mode.PLAY;
                this.source = new File(leftovers.get(0));
            } else {
                String msg = composeErrMsg(
                        String.format("Unrecognized arguments:%s", leftovers.subList(2, leftovers.size())), "-",
                        null);
                throw new CLIException(msg);
            }
        }
    }

    private Map<String, Pattern> initializeMidiPorts(CommandLine cmd, String optionName) throws CLIException {
        Properties props = cmd.getOptionProperties(optionName);
        Map<String, Pattern> ret = new HashMap<String, Pattern>();
        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) {
                String msg = String.format("Regular expression '%' for '%s' isn't a valid regular expression.",
                        portname, p);
                throw new CLIException(composeErrMsg(msg, optionName, null), e);
            }
        }
        return ret;
    }

    private String composeErrMsg(String msg, String optionName, String longOptionName) {
        if (longOptionName != null) {
            return String.format("(-%s/--%s) %s", optionName, longOptionName, msg);
        } else {
            return String.format("(-%s) %s", optionName, msg);
        }
    }

    private String getSingleOptionValue(CommandLine cmd, String optionName) throws CLIException {
        String ret = cmd.getOptionValue(optionName);
        int sz = cmd.getOptionProperties(optionName).size();
        if (sz != 1) {
            String msg = composeErrMsg(
                    String.format("This option requires one and only one value. (found %d times)", sz), optionName,
                    null);
            throw new CLIException(msg);
        }
        return ret;
    }

    public Mode getMode() {
        return this.mode;
    }

    private String license() {
        return "Copyright 2013 Hiroshi Ukai.\n\n"
                + "Licensed under the Apache License, Version 2.0 (the \"License\");"
                + "you may not use this work except in compliance with the License. "
                + "You may obtain a copy of the License in the LICENSE file, or at:\n\n"
                + "http://www.apache.org/licenses/LICENSE-2.0\n\n"
                + "Unless required by applicable law or agreed to in writing, "
                + "software distributed under the License is distributed on an \"AS IS\" "
                + "BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either "
                + "express or implied. See the License for the specific language "
                + "governing permissions and limitations under the License.";
    }

    private String version() {
        String path = "/META-INF/maven/com.github.dakusui/symfonion/pom.properties";
        Properties props = new Properties();
        String version = "(N/A)";
        InputStream stream = getClass().getResourceAsStream(path);
        if (stream != null) {
            try {
                props.load(stream);
                version = props.getProperty("version");
            } catch (IOException e) {
            }
        }
        return version;
    }

    public File getSourceFile() {
        return this.source;
    }

    public File getSinkFile() {
        return this.sink;
    }

    public Map<String, Pattern> getMidiInDefinitions() {
        return this.midiins;
    }

    public Map<String, Pattern> getMidiOutDefinitions() {
        return this.midiouts;
    }

    public Route getRoute() {
        return this.route;
    }

    public static void main(String... args) {
        int exitCode = invoke(System.out, System.err, args);
        System.exit(exitCode);
    }

    public static int invoke(PrintStream stdout, PrintStream stderr, String... args) {
        int ret = 255;
        try {
            CLI cli = new CLI(args);
            cli.mode.invoke(cli, stdout);
            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;
        }
        return ret;
    }

    private static void printError(PrintStream ps, Throwable t) {
        ps.println(String.format("symfonion: %s", t.getMessage()));
    }
}