org.tupelo_schneck.electric.Options.java Source code

Java tutorial

Introduction

Here is the source code for org.tupelo_schneck.electric.Options.java

Source

/*
This file is part of
"it's electric": software for storing and viewing home energy monitoring data
Copyright (C) 2009--2012 Robert R. Tupelo-Schneck <schneck@gmail.com>
http://tupelo-schneck.org/its-electric
    
"it's electric" is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
    
"it's electric" 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 Affero General Public License for more details.
    
You should have received a copy of the GNU Affero General Public License
along with "it's electric", as legal/COPYING-agpl.txt.
If not, see <http://www.gnu.org/licenses/>.
 */

package org.tupelo_schneck.electric;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Properties;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

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.ParseException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tupelo_schneck.electric.current_cost.CurrentCostImporter;

import com.ibm.icu.util.TimeZone;

public class Options extends org.apache.commons.cli.Options {
    private static final String ITS_ELECTRIC_PROPERTIES = "its-electric.properties";

    Log log = LogFactory.getLog(Options.class);

    public static final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

    static {
        System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true");

        // The following allows you can access an https URL without having the certificate in the truststore 
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return null;
            }

            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
            }
        } };
        // Install the all-trusting trust manager 
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });
        } catch (Exception e) {
        }
    }

    public String dbFilename = null;
    public String gatewayURL = "http://TED5000";
    public String username = null;
    public String password = null;
    public String serverLogFilename;
    public byte mtus = 1;
    public int importOverlap = 8;
    public int importInterval = 4; // seconds
    public int longImportInterval = 5 * 60;
    public int numDataPoints = 1000;
    public int maxDataPoints = 5000;
    public int port = 8081;
    public boolean voltage = false;
    public long voltAmpereImportIntervalMS = 0;
    public int kvaThreads = 0;

    public boolean tedOptions;

    public boolean record = true;
    public boolean serve = true;

    public boolean ccOptions;
    public String ccPortName;
    public int ccMaxSensor = 0;
    public boolean ccSumClamps = true;
    public int ccNumberOfClamps = 3;

    public TimeZone recordTimeZone = TimeZone.getDefault();
    public TimeZone serveTimeZone = TimeZone.getDefault();

    public boolean export = false;
    public int startTime;
    public int endTime;
    public int resolution;
    public boolean exportByMTU = true;

    public int deleteUntil;

    @SuppressWarnings("static-access")
    public Options() {
        Option optionFile = OptionBuilder.withLongOpt("config-file")
                .withDescription("file to read config options from").hasArg().withArgName("arg").create();
        this.addOption(optionFile);

        Option noServeOpt = OptionBuilder.withLongOpt("no-serve")
                .withDescription("if present, do not serve Google Visualization data").hasOptionalArg()
                .withArgName(null).create();
        this.addOption(noServeOpt);

        Option noRecordOpt = OptionBuilder.withLongOpt("no-record")
                .withDescription("if present, do not record data from TED").hasOptionalArg().withArgName(null)
                .create();
        this.addOption(noRecordOpt);

        Option exportOpt = OptionBuilder.withLongOpt("export").withDescription(
                "export CSV for existing data from <start> to <end> of resolution <res>; implies --no-serve --no-record")
                .hasArgs(3).withArgName("start> <end> <res").withValueSeparator(' ').create();
        this.addOption(exportOpt);

        Option exportStyleOpt = OptionBuilder.withLongOpt("export-style").withDescription(
                "if 'timestamp', export data is one line per timestamp; otherwise one line per timestamp/mtu pair")
                .hasArg().create();
        this.addOption(exportStyleOpt);

        this.addOption("d", "database-directory", true, "database directory (required)");
        this.addOption("p", "port", true,
                "port served by datasource server (\"none\" same as --no-serve; default 8081)");
        this.addOption("m", "mtus", true, "number of MTUs (default 1)");
        this.addOption("g", "gateway-url", true,
                "URL of TED 5000 gateway (\"none\" same as --no-record; default http://TED5000)");
        this.addOption("u", "username", true,
                "username for password-protected TED gateway (will prompt for password; default none)");
        this.addOption("n", "num-points", true,
                "target number of data points returned over the zoom region (default 1000)");
        this.addOption("x", "max-points", true,
                "number of data points beyond which server will not go (default 5000)");
        this.addOption("l", "server-log", true,
                "server request log filename; include string \"yyyy_mm_dd\" for automatic rollover; or use \"stderr\" (default no log)");
        this.addOption("i", "import-interval", true,
                "seconds between imports of data, or 0 for only hour-long imports (default 4)");
        this.addOption("o", "import-overlap", true,
                "extra seconds imported each time for good measure (default 8)");
        this.addOption("e", "long-import-interval", true,
                "seconds between imports of whole hours, or 0 for only short imports (default 300)");

        Option voltageOpt = OptionBuilder.withLongOpt("voltage")
                .withDescription("whether to include voltage data (default no)").hasOptionalArg().withArgName(null)
                .create("v");
        this.addOption(voltageOpt);

        Option kvaOpt = OptionBuilder.withLongOpt("volt-ampere-import-interval")
                .withDescription(
                        "seconds between polls for kVA data (accepts decimal values; default 0 means no kVA data)")
                .withArgName(null) // save space in help text
                .hasArg().create("k");
        this.addOption(kvaOpt);

        Option kvaThreadsOpt = OptionBuilder.withLongOpt("volt-ampere-threads")
                .withDescription("number of threads for polling kVA (default 0 means share short import thread)")
                .withArgName("arg").hasArg().create();
        this.addOption(kvaThreadsOpt);

        Option timeZoneOpt = OptionBuilder.withLongOpt("record-time-zone").withDescription(
                "time zone for TED Gateway as ISO8601 offset or tz/zoneinfo name (default use time zone of its-electric install)")
                .withArgName("arg").hasArg().create();
        this.addOption(timeZoneOpt);

        timeZoneOpt = OptionBuilder.withLongOpt("serve-time-zone").withDescription(
                "time zone for data service output as ISO8601 offset or tz/zoneinfo name (default use time zone of its-electric install)")
                .withArgName("arg").hasArg().create();
        this.addOption(timeZoneOpt);

        timeZoneOpt = OptionBuilder.withLongOpt("time-zone")
                .withDescription("convenience parameter combining serve-time-zone and record-time-zone")
                .withArgName("arg").hasArg().create();
        this.addOption(timeZoneOpt);

        timeZoneOpt = OptionBuilder.withLongOpt("ted-no-dst")
                .withDescription(
                        "convenience parameter; if set, record-time-zone is set to serve-time-zone with no DST")
                .hasOptionalArg().withArgName(null).create();
        this.addOption(timeZoneOpt);

        Option deleteUntilOpt = OptionBuilder.withLongOpt("delete-until")
                .withDescription(
                        "if present, delete all entries in database up to this time; confirmation required")
                .withArgName("arg").hasArg().create();
        this.addOption(deleteUntilOpt);

        Option ccListSerialPortsOpt = OptionBuilder.withLongOpt("cc-list-serial-ports")
                .withDescription("Current Cost: list all serial ports and exit").create();
        this.addOption(ccListSerialPortsOpt);

        Option ccPortNameOpt = OptionBuilder.withLongOpt("cc-port-name")
                .withDescription("Current Cost: port name; required for Current Cost use").withArgName("arg")
                .hasArg().create();
        this.addOption(ccPortNameOpt);

        Option ccMaxSensorOpt = OptionBuilder.withLongOpt("cc-max-sensor")
                .withDescription("Current Cost: highest sensor number recorded (default 0)").withArgName("arg")
                .hasArg().create();
        this.addOption(ccMaxSensorOpt);

        Option ccSeparateClampsOpt = OptionBuilder.withLongOpt("cc-separate-clamps")
                .withDescription("Current Cost: if present, have separate readings for each clamp").hasOptionalArg()
                .withArgName(null).create();
        this.addOption(ccSeparateClampsOpt);

        Option ccNumClampsOpt = OptionBuilder.withLongOpt("cc-num-clamps").withDescription(
                "Current Cost: if cc-separate-clamps, how many clamps to record for each sensor (default 3)")
                .withArgName("arg").hasArg().create();
        this.addOption(ccNumClampsOpt);

        this.addOption("h", "help", false, "print this help text");
    }

    private boolean optionalBoolean(OptionWrapper cmd, String long_param, String short_param, boolean defaultVal) {
        String val = cmd.getOptionValue(long_param, short_param);
        if (val == null)
            return !defaultVal;
        else {
            val = val.toLowerCase();
            if ("yes".equals(val) || "true".equals(val)) {
                return true;
            }
            if ("no".equals(val) || "false".equals(val)) {
                return false;
            }
            throw new NumberFormatException("expecting true/false or yes/no for " + val);
        }
    }

    private static class FileConfig {
        Properties props = new Properties();

        FileConfig(String fileName) throws IOException {
            props.load(new BufferedReader(new InputStreamReader(new FileInputStream(fileName), "UTF-8")));
        }

        boolean hasOption(String longOpt, String shortOpt) {
            longOpt = (longOpt == null ? "" : longOpt);
            shortOpt = (shortOpt == null ? "" : shortOpt);
            return props.containsKey(longOpt) || props.containsKey(shortOpt);
        }

        String getOptionValue(String longOpt, String shortOpt) {
            if (!hasOption(longOpt, shortOpt))
                return null;
            longOpt = (longOpt == null ? "" : longOpt);
            shortOpt = (shortOpt == null ? "" : shortOpt);

            String optionValue = null;
            optionValue = props.getProperty(longOpt);
            if (optionValue != null && !optionValue.equals(""))
                return optionValue.trim();
            optionValue = props.getProperty(shortOpt);
            if (optionValue != null && !optionValue.equals(""))
                return optionValue.trim();
            return null;
        }
    }

    private class OptionWrapper {
        private final CommandLine cmd;
        private final FileConfig config;

        OptionWrapper(CommandLine cmd, FileConfig config) {
            this.cmd = cmd;
            this.config = config;
        }

        private boolean cmdHasOption(String longOpt, String shortOpt) {
            return (cmd.hasOption(longOpt) || cmd.hasOption(shortOpt));
        }

        private boolean configHasOption(String longOpt, String shortOpt) {
            return ((config != null) ? config.hasOption(longOpt, shortOpt) : false);
        }

        boolean hasOption(String longOpt, String shortOpt) {
            longOpt = (longOpt == null ? "" : longOpt);
            shortOpt = (shortOpt == null ? "" : shortOpt);
            return cmdHasOption(longOpt, shortOpt) || configHasOption(longOpt, shortOpt);
        }

        String getOptionValue(String longOpt, String shortOpt) {
            String optionValue = null;

            longOpt = (longOpt == null ? "" : longOpt);
            shortOpt = (shortOpt == null ? "" : shortOpt);

            Option option = !longOpt.equals("") ? Options.this.getOption(longOpt)
                    : Options.this.getOption(shortOpt);

            if (cmdHasOption(longOpt, shortOpt)) {
                optionValue = cmd.getOptionValue(longOpt);
                if (optionValue == null)
                    optionValue = cmd.getOptionValue(shortOpt);
            } else if (configHasOption(longOpt, shortOpt)) {
                optionValue = config.getOptionValue(longOpt, shortOpt);

                if (optionValue == null && option.hasArg() && !option.hasOptionalArg()) {
                    throw new NumberFormatException(longOpt + "/" + shortOpt + " requires mandatory argument");
                }
                if (optionValue != null && !option.hasArg() && !option.hasOptionalArg()) {
                    throw new NumberFormatException(longOpt + "/" + shortOpt + " does not have argument");
                }
            }
            return optionValue;
        }

        String[] getOptionValues(String longOpt, String shortOpt) {
            String[] optionValues = null;

            longOpt = (longOpt == null ? "" : longOpt);
            shortOpt = (shortOpt == null ? "" : shortOpt);

            Option option = !longOpt.equals("") ? Options.this.getOption(longOpt)
                    : Options.this.getOption(shortOpt);

            if (cmdHasOption(longOpt, shortOpt)) {
                optionValues = cmd.getOptionValues(longOpt);
                if (optionValues == null)
                    optionValues = cmd.getOptionValues(shortOpt);
            } else if (configHasOption(longOpt, shortOpt)) {
                String optionValue = config.getOptionValue(longOpt, shortOpt);

                if (optionValue == null && (option.hasArgs() || option.hasArg())) {
                    throw new NumberFormatException(longOpt + "/" + shortOpt + " requires mandatory argument");
                }
                if (optionValue != null && !option.hasArgs() && !option.hasOptionalArg() && !option.hasArg()) {
                    throw new NumberFormatException(longOpt + "/" + shortOpt + " does require an argument");
                }

                if (optionValue != null) {
                    char argSeparator = option.getValueSeparator();
                    String argSeparatorRegex;
                    if (argSeparator == ' ')
                        argSeparatorRegex = "\\s++";
                    else
                        argSeparatorRegex = Pattern.quote(String.valueOf(argSeparator));
                    optionValues = optionValue.split(argSeparatorRegex);
                }

                if (option.hasArgs() && (optionValues == null || optionValues.length != option.getArgs())) {
                    throw new NumberFormatException(
                            longOpt + "/" + shortOpt + " requires " + option.getArgs() + " mandatory arguments");
                }
            }
            return optionValues;
        }
    }

    /** Returns true if program should continue */
    public boolean parseOptions(String[] args) throws IOException {
        // create the parser
        CommandLineParser parser = new GnuParser();
        CommandLine cmd = null;
        FileConfig config = null;
        OptionWrapper options = null;
        boolean showUsageAndExit = false;
        boolean listSerialPortsAndExit = false;
        boolean hasDbFilename = true;
        try {
            // parse the command line arguments
            cmd = parser.parse(this, args);
        } catch (ParseException exp) {
            // oops, something went wrong
            System.err.println(exp.getMessage());
            showUsageAndExit = true;
        }

        if (cmd == null) {
            showUsageAndExit = true;
        } else {
            try {
                if (cmd.hasOption("config-file")) {
                    String optionFileName = cmd.getOptionValue("config-file");

                    try {
                        config = new FileConfig(optionFileName);
                    } catch (IOException exp) {
                        System.err.println(exp.getMessage());
                        showUsageAndExit = true;
                    }
                }

                options = new OptionWrapper(cmd, config);

                hasDbFilename = true;
                if (options.hasOption("database-directory", "d")) {
                    dbFilename = options.getOptionValue("database-directory", "d");
                } else if (cmd.getArgs().length == 1) {
                    dbFilename = cmd.getArgs()[0];
                } else {
                    hasDbFilename = false;
                }

                if (config == null && hasDbFilename) {
                    File defaultConfigFile = new File(dbFilename, ITS_ELECTRIC_PROPERTIES);
                    if (defaultConfigFile.exists()) {
                        try {
                            config = new FileConfig(defaultConfigFile.getAbsolutePath());
                        } catch (IOException exp) {
                            System.err.println(exp.getMessage());
                            showUsageAndExit = true;
                        }
                        options = new OptionWrapper(cmd, config);
                    }
                }

                if (options.hasOption("no-serve", null)) {
                    serve = !optionalBoolean(options, "no-serve", null, false);
                }
                if (options.hasOption("no-record", null)) {
                    record = !optionalBoolean(options, "no-record", null, false);
                }

                boolean recordChanged = false;
                boolean serveChanged = false;

                if (options.hasOption("time-zone", null)) {
                    String input = options.getOptionValue("time-zone", null);
                    input = convertISO8601TimeZone(input);
                    recordTimeZone = TimeZone.getTimeZone(input);
                    serveTimeZone = TimeZone.getTimeZone(input);
                    recordChanged = true;
                    serveChanged = true;
                }

                if (options.hasOption("serve-time-zone", null)) {
                    String input = options.getOptionValue("serve-time-zone", null);
                    input = convertISO8601TimeZone(input);
                    serveTimeZone = TimeZone.getTimeZone(input);
                    serveChanged = true;
                }

                if (options.hasOption("ted-no-dst", null)) {
                    if (optionalBoolean(options, "ted-no-dst", null, false)) {
                        int offset = serveTimeZone.getRawOffset();
                        boolean negative = offset < 0;
                        if (negative)
                            offset = -offset;
                        int hours = offset / 3600000;
                        offset = offset - 3600000 * hours;
                        int minutes = offset / 60000;
                        String input = "GMT" + (negative ? "-" : "+") + (hours < 10 ? "0" : "") + hours
                                + (minutes < 10 ? "0" : "") + minutes;
                        recordTimeZone = TimeZone.getTimeZone(input);
                        recordChanged = true;
                    }
                }

                if (options.hasOption("record-time-zone", null)) {
                    String input = options.getOptionValue("record-time-zone", null);
                    input = convertISO8601TimeZone(input);
                    recordTimeZone = TimeZone.getTimeZone(input);
                    recordChanged = true;
                }

                if (recordChanged)
                    log.info("Record Time Zone: " + TimeZone.getCanonicalID(recordTimeZone.getID()) + ", "
                            + recordTimeZone.getDisplayName());
                if (serveChanged)
                    log.info("Serve Time Zone: " + TimeZone.getCanonicalID(serveTimeZone.getID()) + ", "
                            + serveTimeZone.getDisplayName());

                if (options.hasOption("export", null)) {
                    serve = false;
                    record = false;
                    export = true;
                    String[] vals = options.getOptionValues("export", null);
                    startTime = Util.timestampFromUserInput(vals[0], false, serveTimeZone);
                    endTime = Util.timestampFromUserInput(vals[1], true, serveTimeZone);
                    resolution = Integer.parseInt(vals[2]);
                }
                if (options.hasOption("export-style", null)) {
                    exportByMTU = !options.getOptionValue("export-style", null).trim().toLowerCase()
                            .equals("timestamp");
                }

                if (options.hasOption("delete-until", null)) {
                    deleteUntil = Util.timestampFromUserInput(options.getOptionValue("delete-until", null), false,
                            serveTimeZone);
                }

                if (options.hasOption("port", "p")) {
                    String val = options.getOptionValue("port", "p");
                    if (val.equals("none")) {
                        serve = false;
                    } else {
                        port = Integer.parseInt(val);
                        if (port <= 0)
                            showUsageAndExit = true;
                    }
                }
                if (options.hasOption("gateway-url", "g")) {
                    tedOptions = true;
                    gatewayURL = options.getOptionValue("gateway-url", "g");
                    if (gatewayURL.equals("none"))
                        record = false;
                }
                if (options.hasOption("mtus", "m")) {
                    tedOptions = true;
                    mtus = Byte.parseByte(options.getOptionValue("mtus", "m"));
                    if (mtus <= 0 || mtus > 4)
                        showUsageAndExit = true;
                }
                if (options.hasOption("username", "u")) {
                    tedOptions = true;
                    username = options.getOptionValue("username", "u");
                }
                if (options.hasOption("num-points", "n")) {
                    numDataPoints = Integer.parseInt(options.getOptionValue("num-points", "n"));
                    if (numDataPoints <= 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("max-points", "x")) {
                    maxDataPoints = Integer.parseInt(options.getOptionValue("max-points", "x"));
                    if (maxDataPoints <= 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("import-interval", "i")) {
                    tedOptions = true;
                    importInterval = Integer.parseInt(options.getOptionValue("import-interval", "i"));
                    if (importInterval < 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("import-overlap", "o")) {
                    tedOptions = true;
                    importOverlap = Integer.parseInt(options.getOptionValue("import-overlap", "o"));
                    if (importOverlap < 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("long-import-interval", "e")) {
                    tedOptions = true;
                    longImportInterval = Integer.parseInt(options.getOptionValue("long-import-interval", "e"));
                    if (longImportInterval < 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("server-log", "l")) {
                    serverLogFilename = options.getOptionValue("server-log", "l");
                }
                if (options.hasOption("voltage", "v")) {
                    tedOptions = true;
                    voltage = optionalBoolean(options, "voltage", "v", false);
                }
                if (options.hasOption("volt-ampere-import-interval", "k")) {
                    tedOptions = true;
                    double value = Double.parseDouble(options.getOptionValue("volt-ampere-import-interval", "k"));
                    voltAmpereImportIntervalMS = (long) (1000 * value);
                    if (voltAmpereImportIntervalMS < 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("volt-ampere-threads", null)) {
                    tedOptions = true;
                    kvaThreads = Integer.parseInt(options.getOptionValue("volt-ampere-threads", null));
                    if (kvaThreads < 0)
                        showUsageAndExit = true;
                }
                if (options.hasOption("cc-port-name", null)) {
                    ccOptions = true;
                    ccPortName = options.getOptionValue("cc-port-name", null);
                }
                if (options.hasOption("cc-max-sensor", null)) {
                    ccOptions = true;
                    ccMaxSensor = Integer.parseInt(options.getOptionValue("cc-max-sensor", null));
                }
                if (options.hasOption("cc-separate-clamps", null)) {
                    ccOptions = true;
                    ccSumClamps = !optionalBoolean(options, "cc-separate-clamps", null, false);
                }
                if (options.hasOption("cc-num-clamps", null)) {
                    ccOptions = true;
                    ccNumberOfClamps = Integer.parseInt(options.getOptionValue("cc-num-clamps", null));
                }
                if (options.hasOption("help", "h")) {
                    showUsageAndExit = true;
                }

                if (options.hasOption("cc-list-serial-ports", null)) {
                    listSerialPortsAndExit = true;
                }
            } catch (NumberFormatException e) {
                System.err.println(e.getMessage());
                showUsageAndExit = true;
            }
        }

        if (!hasDbFilename && !listSerialPortsAndExit) {
            showUsageAndExit = true;
        } else if (!serve && !record && !export && deleteUntil == 0 && !listSerialPortsAndExit) {
            showUsageAndExit = true;
        }

        if (ccOptions && tedOptions) {
            System.out.println("Cannot combine TED and Current Cost options.");
            showUsageAndExit = true;
        } else if (ccOptions && ccPortName == null) {
            System.out.println("Current Cost use requires --cc-port-name.");
            showUsageAndExit = true;
        }

        if (showUsageAndExit) {
            showUsage();
            return false;
        } else if (listSerialPortsAndExit) {
            CurrentCostImporter.printSerialPortNames();
            return false;
        } else {
            if (record && username != null) {
                readAndProcessPassword();
            }
            confirmDeleteUntil();
            return true;
        }
    }

    private void showUsage() {
        StringBuilder header = new StringBuilder();
        header.append("\n");
        header.append("The \"it's electric\" Java program is designed to perform two simultaneous\n");
        header.append("activities:\n");
        header.append("(1) it records data from TED into a permanent database; and\n");
        header.append("(2) it serves data from the database in Google Visualization API format.\n");
        header.append("Additionally, the program can\n");
        header.append("(3) export data from the database in CSV format.\n");
        header.append("\n");
        header.append("To export data, use: java -jar its-electric-*.jar -d <database-directory>\n");
        header.append("                                      --export <start> <end> <resolution>\n");
        header.append("\n");
        header.append("You can specify to only record data using option --no-serve (e.g. for an\n");
        header.append("unattended setup) and to only serve data using option --no-record (e.g. with a\n");
        header.append("static copy of an its-electric database).\n");
        header.append("\n");
        header.append("Options -d (specifying the database directory) and -m (specifying the number of\n");
        header.append("MTUs) are important for both recording and serving.  Option -g (the TED Gateway\n");
        header.append("URL) and options -v and -k (which determine whether to record voltage and\n");
        header.append("volt-amperes) are important for recording.  Option -p (the port on which to\n");
        header.append("serve) is important for serving.  Other options are generally minor.\n");
        header.append("\n");
        header.append("Options (-d is REQUIRED):");

        HelpFormatter help = new HelpFormatter();
        PrintWriter writer = new PrintWriter(System.out);
        writer.println("usage: java -jar its-electric-*.jar [options]");
        writer.println(header.toString());
        help.printOptions(writer, 80, this, 0, 0);
        writer.flush();
        writer.close();
    }

    private void readAndProcessPassword() throws IOException {
        System.err.print("Please enter password for username '" + username + "': ");
        password = Options.reader.readLine();
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password.toCharArray());
            }
        });
    }

    private void confirmDeleteUntil() {
        if (deleteUntil > 1230000000) {
            boolean doit;
            System.err.print(
                    "Irrevocably delete all data up to " + Util.dateString(deleteUntil) + "? (yes/no [no]) ");
            try {
                String input = Options.reader.readLine();
                doit = input != null && input.toLowerCase().trim().equals("yes");
            } catch (IOException e) {
                doit = false;
            }
            if (!doit)
                deleteUntil = 0;
        }
    }

    private String convertISO8601TimeZone(String input) {
        Matcher m = Pattern.compile("([+-]\\d{2}):?+(\\d{2})").matcher(input);
        if (m.matches()) {
            input = "GMT" + m.group(1) + m.group(2);
        }
        return input;
    }
}