com.tesora.dve.tools.DVEAnalyzerCLI.java Source code

Java tutorial

Introduction

Here is the source code for com.tesora.dve.tools.DVEAnalyzerCLI.java

Source

package com.tesora.dve.tools;

/*
 * #%L
 * Tesora Inc.
 * Database Virtualization Engine
 * %%
 * Copyright (C) 2011 - 2014 Tesora Inc.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 * 
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.PrintStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.InputMismatchException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;

import com.google.common.collect.ImmutableSet;
import com.tesora.dve.common.DBHelper;
import com.tesora.dve.common.DBType;
import com.tesora.dve.common.MultiMap;
import com.tesora.dve.common.PEConstants;
import com.tesora.dve.common.PEFileUtils;
import com.tesora.dve.common.PEUrl;
import com.tesora.dve.common.PEXmlUtils;
import com.tesora.dve.db.DBNative;
import com.tesora.dve.exceptions.PEException;
import com.tesora.dve.server.bootstrap.Host;
import com.tesora.dve.sql.schema.QualifiedName;
import com.tesora.dve.sql.schema.UnqualifiedName;
import com.tesora.dve.sql.template.jaxb.ModelType;
import com.tesora.dve.sql.template.jaxb.TableTemplateType;
import com.tesora.dve.sql.template.jaxb.Template;
import com.tesora.dve.tools.aitemplatebuilder.AiTemplateBuilder;
import com.tesora.dve.tools.aitemplatebuilder.CorpusStats;
import com.tesora.dve.tools.aitemplatebuilder.CorpusStats.TableStats;
import com.tesora.dve.tools.aitemplatebuilder.TemplateModelItem;
import com.tesora.dve.tools.analyzer.Analyzer;
import com.tesora.dve.tools.analyzer.AnalyzerCallback;
import com.tesora.dve.tools.analyzer.AnalyzerOption;
import com.tesora.dve.tools.analyzer.AnalyzerOptions;
import com.tesora.dve.tools.analyzer.AnalyzerPlanningError;
import com.tesora.dve.tools.analyzer.AnalyzerPlanningNotice;
import com.tesora.dve.tools.analyzer.AnalyzerPlanningResult;
import com.tesora.dve.tools.analyzer.AnalyzerResult;
import com.tesora.dve.tools.analyzer.AnalyzerSource;
import com.tesora.dve.tools.analyzer.Emulator;
import com.tesora.dve.tools.analyzer.StatementCounter;
import com.tesora.dve.tools.analyzer.StaticAnalyzer;
import com.tesora.dve.tools.analyzer.jaxb.AnalyzerType;
import com.tesora.dve.tools.analyzer.jaxb.DatabasesType.Database;
import com.tesora.dve.tools.analyzer.jaxb.DbAnalyzerCorpus;
import com.tesora.dve.tools.analyzer.jaxb.DbAnalyzerReport;
import com.tesora.dve.tools.analyzer.jaxb.HasStatement;
import com.tesora.dve.tools.analyzer.jaxb.StatementNonDMLType;
import com.tesora.dve.tools.analyzer.jaxb.StatementPopulationType;
import com.tesora.dve.tools.analyzer.jaxb.TablesType.Table;
import com.tesora.dve.tools.analyzer.sources.FileSource;
import com.tesora.dve.tools.analyzer.sources.FrequenciesSource;
import com.tesora.dve.tools.analyzer.sources.JdbcSource;
import com.tesora.dve.tools.analyzer.stats.BasicStatsVisitor;
import com.tesora.dve.tools.analyzer.stats.StatementAnalyzer;

public class DVEAnalyzerCLI extends CLIBuilder {
    public static final String REPORT_FILE_EXTENSION = ".report";
    public static final String TEMPLATE_FILE_EXTENSION = ".template";
    public static final String CORPUS_FILE_EXTENSION = ".corpus";
    private static final String TEMP_FILE_EXTENSION = ".temp";
    private static final String ERROR_FILE_EXTENSION = ".error";
    private static final String DEFAULT_REPORT_NAME = "analyzer" + REPORT_FILE_EXTENSION;

    private static final DbAnalyzerCorpus EMPTY_FREQUENCY_CORPUS = new DbAnalyzerCorpus();

    private String url = PEConstants.MYSQL_URL_3307;
    private String user = PEConstants.ROOT;
    private String password = PEConstants.PASSWORD;

    private final DBNative dbNative = DBNative.DBNativeFactory.newInstance(DBType.MYSQL);

    private final List<String> databases = new ArrayList<String>();
    private final Map<String, Template> templates = new HashMap<String, Template>();
    private final AnalyzerOptions options = new AnalyzerOptions();

    private DbAnalyzerReport report = null;

    public DVEAnalyzerCLI(String[] args) throws PEException {
        super(args, "Analyzer Tool");

        registerCommand(new CommandType(new String[] { "connect" }, "<url> <user> <password>",
                "Connect to the specified database (default=" + PEConstants.MYSQL_URL + " " + PEConstants.ROOT + " "
                        + PEConstants.PASSWORD + ")."));

        registerCommand(new CommandType(new String[] { "set", "database" }, "<database> [<database>]...",
                "Set the name of the database to use for analysis."));

        registerCommand(new CommandType(new String[] { "static" }, "[<analyze>]",
                "Extract static analysis information from the database."
                        + "If TRUE <analyze> executes 'ANALYZE TABLE' command before collecting table statistics (preferred, but causes update to the INFORMATION_SCHEMA and requires INSERT privileges)."));

        registerCommand(new CommandType(new String[] { "open", "report" }, "[<filename>]",
                "Open an existing static analysis file (default filename = " + DEFAULT_REPORT_NAME + ")."));
        registerCommand(new CommandType(new String[] { "save", "report" }, "[<filename>]",
                "Write static analysis data to the specified file (default filename = " + DEFAULT_REPORT_NAME
                        + ")."));
        registerCommand(new CommandType(new String[] { "display", "report" }, "Display the current analysis."));
        registerCommand(new CommandType(new String[] { "update", "row", "counts" }, "<filename>",
                "Replace row counts in the current static report by values from a given tab-delimited file obtained by 'select * from information_schema.tables;' (must include column headers)."));

        registerCommand(new CommandType(new String[] { "generate", "broadcast", "templates" },
                "Generate all-broadcast templates."));
        registerCommand(new CommandType(new String[] { "generate", "random", "templates" },
                "Generate all-random templates."));
        registerCommand(new CommandType(new String[] { "generate", "basic", "templates" },
                "<cutoff cardinality> [<base template>]",
                "Generate broadcast/random templates with a hard broadcast cardinality cutoff."));
        registerCommand(new CommandType(new String[] { "generate", "guided", "templates" },
                "<cutoff cardinality> <fk> <safe> [<corpus file>] [<base template>]",
                "Generate templates with a hard broadcast cardinality cutoff. See \"generate templates\" command for the definition of other parameters."));
        registerCommand(new CommandType(new String[] { "generate", "templates" },
                "<fk> <safe> [<corpus file>] [<base template>]",
                "Generate templates from a given frequency corpus."
                        + " If TRUE the <fk> switch forces the generator to coolocate the foreign keys."
                        + " If TRUE the <safe> switch forces the generator to generate only ranges backed-up by auto-increment columns."
                        + " Note that colocating foreign keys alone generally requires heavy broadcasting,"
                        + " turning both <fk> and <safe> switches ON at the same time may easily lead to unacceptable broadcasting in the template."
                        + " Use with caution."
                        + " A corpus file provides important information on statements executed agains the schema."
                        + " The corpus file should ideally cover majority of the schema tables (> 60%)."
                        + " It is generally necessary for a good template, but is not strictly required,"
                        + " as table cardinalities and FK relationships can be resolved from the static report alone."));

        registerCommand(new CommandType(new String[] { "open", "template" }, "<filename>",
                "Open a template from the specified file."));
        registerCommand(new CommandType(new String[] { "open", "templates" }, "[<folder>]",
                "Open all template files in the specified folder (default folder = current working directory)."));
        registerCommand(new CommandType(new String[] { "save", "template" }, "<template name> [<filename>]",
                "Write the specified template to a file (default filename = <template name>"
                        + TEMPLATE_FILE_EXTENSION + ")."));
        registerCommand(new CommandType(new String[] { "save", "templates" }, "[<folder>]",
                "Write all templates to the specified folder (default folder = current working directory)."));
        registerCommand(new CommandType(new String[] { "display", "template" }, "[<template name>]",
                "Display the specified template or all templates if name isn't specified."));
        registerCommand(new CommandType(new String[] { "compare", "templates" },
                "<template 1> <template 2> [<output file>]", "Compare given template files."));
        registerCommand(new CommandType(new String[] { "advanced", "compare", "templates" },
                "<corpus file> <template 1> <template 2> [<output file>]",
                "Compare given template files. Includes table statistics in the output. This comparator requires a static report and frequency corpus."));

        registerCommand(new CommandType(new String[] { "dynamic", "table" },
                "<connection-url> <username> <password> <log table name> [<output file>]",
                "Perform dynamic analysis using specified mysql query log table on specified connection."));
        registerCommand(new CommandType(new String[] { "dynamic", "plain" }, "<query log file> [<output file>]",
                "Perform dynamic analysis using the specified file of queries (one per line)."));
        registerCommand(new CommandType(new String[] { "dynamic", "mysql" }, "<mysql log file> [<output file>]",
                "Perform dynamic analysis using specified mysql query log."));
        registerCommand(new CommandType(new String[] { "dynamic", "corpus" }, "<corpus file> [<output file>]",
                "Perform dynamic analysis on the specified frequency corpus file."));

        registerCommand(new CommandType(new String[] { "frequencies", "table" },
                "<corpus file> <connection-url> <username> <password> <log table name> [<log table name> ...]",
                "Perform frequency analysis using specified mysql query log table on specified connection, writing the output to the specified <corpus file>."));
        registerCommand(new CommandType(new String[] { "frequencies", "plain" },
                "<corpus file> <query log file> [<query log file> ...]",
                "Perform frequency analysis on specified query log files (one per line), writing the output to the specified <corpus file>."));
        registerCommand(new CommandType(new String[] { "frequencies", "mysql" },
                "<corpus file> <mysql log file> [<mysql log file> ...]",
                "Perform frequency analysis on specified mysql log files, writing the output to the specified <corpus file>."));
        registerCommand(new CommandType(new String[] { "merge", "corpus" },
                "<output corpus file> <merged file 1> <merged file 2> [<merged file> ...]",
                "Merge multiple corpuses into a single file updating the counts on repeated statements."));

        registerCommand(new CommandType(new String[] { "analyze", "corpus" }, "<corpus file>",
                "Analyze statements in the specified corpus file."));

        registerCommand(new CommandType(new String[] { "option" }, "<key> <value>", "Set analysis option"));
        registerCommand(new CommandType(new String[] { "options" },
                "Display available options, along with current values and a description."));

        registerCommand(
                new CommandType(new String[] { "status" }, "Show the current tool status and configuration."));
        registerCommand(new CommandType(new String[] { "reset" }, "Reset to initial state."));

        // Create a dummy Host with minimum configuration
        final Properties props = new Properties();
        props.put(DBHelper.CONN_DRIVER_CLASS, PEConstants.MYSQL_DRIVER_CLASS);
        new Host(props, false);
        super.setDebugMode(true); //  Toggle the debug mode on for the Analyzer.
    }

    /**
     * Specify the connection information to use when connecting to a database.
     * This information is used when the 'static' command is executed.
     * Note: this information is persisted to and read back from the report file
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_connect(Scanner scanner) throws PEException {
        if (scanner.hasNext()) {
            url = scan(scanner, "jdbcURL");
            user = scan(scanner, "user");
            password = scan(scanner, "password");
        }

        url = PEUrl.fromUrlString(url).getURL();

        println("... Connection set to '" + url.toString() + "'");
    }

    /**
     * Reset the analyzer to initial default settings
     */
    public void cmd_reset() {
        report = null;
        clearDatabases();
        clearTemplates();

        url = PEConstants.MYSQL_URL_3307;
        user = PEConstants.ROOT;
        password = PEConstants.PASSWORD;

        options.reset();

        println("... Reset to initial settings");
    }

    /**
     * Dump information to the console about the current configuration of the
     * tool
     */
    public void cmd_status() {
        println("Current Settings:");
        println("    URL       = " + url);
        println("    User      = " + user);
        println("    Password  = " + password);
        println("    Databases = " + databasesToString());
        println("    Options:");
        for (final AnalyzerOption ao : options.getOptions()) {
            println("        " + ao.getName() + " = " + ao.getCurrentValue());
        }
    }

    /**
     * Perform static analysis of a database. In order to run the database(s)
     * must have been set.
     * 
     * @throws PEException
     * @throws SQLException
     */
    public void cmd_static(Scanner scanner) throws PEException, SQLException {
        checkConnection();
        checkDatabase();

        final Boolean analyze = BooleanUtils.toBoolean(scan(scanner));

        println("... Connecting to " + url + " as " + user + "/" + password);
        final DBHelper dbHelper = new DBHelper(url, user, password).connect();

        println("... Starting static analysis for '" + databasesToString() + "'");
        report = StaticAnalyzer.doStatic(dbNative, options, url, user, password, databases, dbHelper, analyze);

        println("... Static anlysis complete for '" + databasesToString() + "'");
    }

    /**
     * Open the specified report file and use it to configure the analyzer
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_open_report(Scanner scanner) throws PEException {
        File reportFile = scanFile(scanner);

        if (reportFile == null) {
            reportFile = new File(DEFAULT_REPORT_NAME);
        }

        println("... Reading analyzer report from file '" + reportFile.getAbsolutePath() + "'");

        report = PEXmlUtils.unmarshalJAXB(reportFile, DbAnalyzerReport.class);

        // Setup the list of databases based on those represented in the
        // report
        clearDatabases();
        for (final Database db : report.getDatabases().getDatabase()) {
            databases.add(db.getName());
        }
        println("... Using databases '" + databasesToString() + "'");

        // Fill in other global settings
        final AnalyzerType.Connection c = report.getAnalyzer().getConnection();
        if (c != null) {
            user = c.getUser();
            password = c.getPassword();
            url = c.getUrl();
        }

        final AnalyzerType.Options os = report.getAnalyzer().getOptions();
        if (os != null) {
            for (final AnalyzerType.Options.Option o : os.getOption()) {
                try {
                    options.setOption(o.getKey(), o.getValue());
                } catch (final PEException e) {
                    println("WARNING: " + e.getMessage() + ". Will be ignored.");
                }
            }
        }
    }

    /**
     * Save the analyzer report file to the specified filename
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_save_report(Scanner scanner) throws PEException {
        checkReport();

        // Filename is optional - if not set then pick a default
        File file = scanFile(scanner);

        if (file == null) {
            file = new File(DEFAULT_REPORT_NAME);
        }

        println("... Writing analyzer report to file '" + file.getAbsolutePath() + "'");
        writeToFileOrScreen(file, PEXmlUtils.marshalJAXB(report));
    }

    /**
     * Dump the contents of the analyzer report to screen or to a file
     * 
     * @throws PEException
     */
    public void cmd_display_report() throws PEException {
        checkReport();

        writeToFileOrScreen(null, PEXmlUtils.marshalJAXB(report));
    }

    /**
     * Update row counts in a given static report.
     * 
     * @throws PEException
     * @throws FileNotFoundException
     */
    public void cmd_update_row_counts(Scanner scanner) throws PEException, FileNotFoundException {
        checkDatabase();
        checkReport();

        final File file = scanFile(scanner, "row counts file");

        println("... Reading row counts from file '" + file.getAbsolutePath() + "'");

        final Map<QualifiedName, Integer> rowCounts = getRowCountsFromFile(file);

        for (final Database staticReportDb : this.report.getDatabases().getDatabase()) {
            for (final Table staticReportTable : staticReportDb.getTables().getTable()) {
                final QualifiedName tableName = new QualifiedName(new UnqualifiedName(staticReportDb.getName()),
                        new UnqualifiedName(staticReportTable.getName()));
                final Integer newCount = rowCounts.get(tableName);
                if (newCount != null) {
                    staticReportTable.setRowCount(newCount);
                }
            }
        }
    }

    /**
     * Run dynamic analysis using the specified general log table
     * 
     * @param scanner
     * @throws Throwable
     */
    @SuppressWarnings({ "synthetic-access", "resource" })
    public void cmd_dynamic_table(Scanner scanner) throws Throwable {
        final String gltUrl = scan(scanner, "general log table url");
        final String gltUsername = scan(scanner, "general log table username");
        final String gltPassword = scan(scanner, "general log table password");
        final String gltLogTable = scan(scanner, "general log table");
        final File outputFile = scanFile(scanner);

        executeDynamicCmdOnAnalyzerSources(
                Collections.singleton(new JdbcSource(gltUrl, gltUsername, gltPassword, gltLogTable)), outputFile,
                new DynamicAnalyzerCallback());
    }

    /**
     * Run dynamic analysis using the specified plain log file
     * 
     * @param scanner
     * @throws Throwable
     */
    @SuppressWarnings({ "synthetic-access", "resource" })
    public void cmd_dynamic_plain(Scanner scanner) throws Throwable {
        final File logFile = scanFile(scanner, "plain log file");
        final File outputFile = scanFile(scanner);

        executeDynamicCmdOnAnalyzerSources(Collections.singleton(new FileSource("plain", logFile)), outputFile,
                new DynamicAnalyzerCallback());
    }

    /**
     * Run dynamic analysis using the specified general log file
     * 
     * @param scanner
     * @throws Throwable
     */
    @SuppressWarnings({ "synthetic-access", "resource" })
    public void cmd_dynamic_mysql(Scanner scanner) throws Throwable {
        final File logFile = scanFile(scanner, "mysql log file");
        final File outputFile = scanFile(scanner);

        executeDynamicCmdOnAnalyzerSources(Collections.singleton(new FileSource("mysql", logFile)), outputFile,
                new DynamicAnalyzerCallback());
    }

    /**
     * Run dynamic analysis using the specified frequency corpus
     * 
     * @param scanner
     * @throws Throwable
     */
    @SuppressWarnings("resource")
    public void cmd_dynamic_corpus(Scanner scanner) throws Throwable {
        final File corpusFile = scanFile(scanner, "corpus file");
        final File output = scanFile(scanner);

        final DbAnalyzerCorpus corpus = loadCorpus(corpusFile);

        executeDynamicCmdOnAnalyzerSources(Collections.singleton(new FrequenciesSource(corpus)), output,
                new DynamicAnalyzerCallback());
    }

    private DbAnalyzerCorpus loadCorpus(File file) throws PEException {
        println("... Reading corpus from file '" + file.getAbsolutePath() + "'");
        return PEXmlUtils.unmarshalJAXB(file, DbAnalyzerCorpus.class);
    }

    public void cmd_frequencies_table(Scanner scanner) throws Throwable {
        final File corpusFile = scanFile(scanner, "corpus file");

        final String gltUrl = scan(scanner, "general log table url");
        final String gltUsername = scan(scanner, "general log table username");
        final String gltPassword = scan(scanner, "general log table password");

        final Set<JdbcSource> sources = this.scanJdbcSources(scanner, gltUrl, gltUsername, gltPassword);
        this.executeFrequenciesCmdOnAnalyzerSources(sources, corpusFile);
    }

    public void cmd_frequencies_plain(Scanner scanner) throws Throwable {
        this.executeFrequenciesCmdOnFileSources(scanner, "plain");
    }

    public void cmd_frequencies_mysql(Scanner scanner) throws Throwable {
        this.executeFrequenciesCmdOnFileSources(scanner, "mysql");
    }

    public void cmd_merge_corpus(Scanner scanner) throws Throwable {
        final File corpusFile = scanFile(scanner, "output corpus file");
        final MultiMap<Class<?>, File> varargs = scanFilesByTypeOptionalMulti(scanner,
                Collections.<Class<?>>singleton(DbAnalyzerCorpus.class));
        final Collection<File> mergedCorpusFiles = varargs.values();
        if (mergedCorpusFiles.size() < 2) {
            throw new PEException("You must provide at least two files for merging");
        }

        final Map<String, StatementNonDMLType> nonDmlPopulations = new HashMap<String, StatementNonDMLType>();
        final Map<String, StatementPopulationType> dmlPopulations = new HashMap<String, StatementPopulationType>();
        final StringBuilder description = new StringBuilder();
        for (final File cf : mergedCorpusFiles) {
            final DbAnalyzerCorpus input = PEXmlUtils.unmarshalJAXB(cf, DbAnalyzerCorpus.class);
            countCorpusStatements(input, nonDmlPopulations, dmlPopulations);
            if (description.length() > 0) {
                description.append("; ");
            } else {
                description.append("Formed by merging: ");
            }
            description.append(input.getDescription());
        }

        final DbAnalyzerCorpus output = new DbAnalyzerCorpus();
        output.getNonDml().addAll(nonDmlPopulations.values());
        output.getPopulation().addAll(dmlPopulations.values());
        output.setDescription(description.toString());

        final String corpusString = PEXmlUtils.marshalJAXB(output);
        writeToFileOrScreen(corpusFile, corpusString);
    }

    private static void countCorpusStatements(final DbAnalyzerCorpus corpus,
            final Map<String, StatementNonDMLType> nonDmlPopulations,
            final Map<String, StatementPopulationType> dmlPopulations) {
        countFrequencies(corpus.getNonDml(), nonDmlPopulations);
        countFrequencies(corpus.getPopulation(), dmlPopulations);
    }

    private static <T extends HasStatement> void countFrequencies(final List<T> stmts,
            final Map<String, T> container) {
        for (final T entry : stmts) {
            bumpFrequency(entry, container);
        }
    }

    private static <T extends HasStatement> void bumpFrequency(final T item, final Map<String, T> container) {
        final T entry = container.get(item.getStmt());
        if (entry == null) {
            container.put(item.getStmt(), item);
        } else {
            final int freq = entry.getFreq() + item.getFreq();
            entry.setFreq(freq);
        }
    }

    private StatementCounter getInitializedStatementCounter(final File corpusFile) throws Throwable {
        checkDatabase();
        checkReport();

        final File reportFile = new File(corpusFile.getAbsolutePath() + TEMP_FILE_EXTENSION);
        final File errorFile = new File(corpusFile.getAbsolutePath() + ERROR_FILE_EXTENSION);

        final StatementCounter sc = new StatementCounter(options, corpusFile, reportFile,
                options.getFrequencyAnalysisCheckpointInterval(), errorFile, this.getPrintStream());

        /*
         * Here we generate initial all-broadcast templates, so that the command
         * "never fails" and allows the user to proceed with advanced template
         * generation.
         */
        generateAllBroadcastTemplates();

        try {
            loadSchema(sc);
        } catch (final Throwable e) {
            clearTemplates();

            throw e;
        }

        return sc;
    }

    private Emulator getInitializedEmulator(final AnalyzerCallback callback, final File outputFile)
            throws Throwable {
        checkDatabase();
        checkReport();
        checkTemplate();

        final Emulator em = new Emulator(options, callback);

        loadSchema(em);

        if (outputFile != null) {
            try {
                callback.setOutputStream(new PrintStream(outputFile));
                println("... Results saved to file '" + outputFile.getAbsolutePath() + "'");
            } catch (final Exception e) {
                throw new PEException("Failed to write to file '" + outputFile.getAbsolutePath() + "'", e);
            }
        }

        return em;
    }

    /**
     * Set the value of a specified analyzer option
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_option(Scanner scanner) throws PEException {
        final String key = scan(scanner, "option key");
        final String value = scan(scanner, "option value");
        options.setOption(key, value);
    }

    /**
     * Output the current set of options and their values
     */
    public void cmd_options() {
        for (final AnalyzerOption ao : options.getOptions()) {
            println(ao.getName() + " = " + ao.getCurrentValue() + ".  " + ao.getDefinition());
        }
    }

    /**
     * Open a template from the specified file. The template name must match on
     * of the database we are analyzing
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_open_template(Scanner scanner) throws PEException {
        final File templateFile = scanFile(scanner, "filename");
        addTemplate(templateFile);
    }

    /**
     * Open all templates from the specified folder or current working
     * directory. Templates are identified by their file extension
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_open_templates(Scanner scanner) throws PEException {
        final File folder = scanFile(scanner, "folder");

        if (!folder.isDirectory()) {
            throw new PEException("'" + folder.getAbsolutePath() + "' is not a valid directory");
        }

        final File[] templateFiles = folder.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(final File dir, final String name) {
                return name.toLowerCase().endsWith(TEMPLATE_FILE_EXTENSION);
            }
        });

        for (final File templateFile : templateFiles) {
            addTemplate(templateFile);
        }
    }

    /**
     * Write the specified template out to a file. The filename is either
     * specified or based on the template name
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_save_template(Scanner scanner) throws PEException {
        checkTemplate();

        final String name = scan(scanner, "template");
        File file = scanFile(scanner);

        if (file == null) {
            file = new File(name + TEMPLATE_FILE_EXTENSION);
        }

        println("... Writing template to file '" + file + "'");

        writeToFileOrScreen(file, PEXmlUtils.marshalJAXB(checkTemplate(name)));
    }

    /**
     * Write all templates to the specified folder or current working directory
     * if folder not specified
     * 
     * @param scanner
     * @throws PEException
     */
    public void cmd_save_templates(Scanner scanner) throws PEException {
        checkTemplate();

        final File folder = scanFile(scanner);
        if ((folder != null) && !folder.isDirectory()) {
            throw new PEException("'" + folder.getAbsolutePath() + "' is not a valid directory.");
        }

        for (final Template t : this.templates.values()) {
            final File file = new File(folder, t.getName() + TEMPLATE_FILE_EXTENSION);

            println("... Writing template to file '" + file.getAbsolutePath() + "'");

            writeToFileOrScreen(file, PEXmlUtils.marshalJAXB(t));
        }
    }

    /**
     * Display one or more templates to the console
     * 
     * @throws PEException
     */
    public void cmd_display_template(Scanner scanner) throws PEException {
        checkTemplate();

        final String name = scan(scanner);

        if (name == null) {
            for (final Template t : templates.values()) {
                writeToFileOrScreen(null, PEXmlUtils.marshalJAXB(t));
            }
        } else {
            writeToFileOrScreen(null, PEXmlUtils.marshalJAXB(checkTemplate(name)));
        }
    }

    /**
     * Generate all-broadcast templates for selected databases.
     */
    public void cmd_generate_broadcast_templates() throws PEException {
        checkReport();
        checkDatabase();

        generateAllBroadcastTemplates();
    }

    /**
     * Generate all-random templates for selected databases.
     */
    public void cmd_generate_random_templates() throws PEException {
        checkReport();
        checkDatabase();

        generateAllRandomTemplates();
    }

    /**
     * Generate broadcast/random templates with a hard broadcast cardinality
     * cutoff.
     */
    public void cmd_generate_basic_templates(final Scanner scanner) throws Exception {
        checkReport();
        checkDatabase();

        final long broadcastCardinalityCutoff = scanLong(scanner, "cutoff cardinality");
        final File baseTemplateFile = scanFile(scanner, null);

        generateBroadcastCutoffTemplates(broadcastCardinalityCutoff, baseTemplateFile);
    }

    /**
     * Generate guided templates with a hard broadcast cardinality cutoff.
     * @throws Throwable 
     */
    public void cmd_generate_guided_templates(final Scanner scanner) throws Throwable {
        checkReport();
        checkDatabase();

        final long broadcastCardinalityCutoff = scanLong(scanner, "cutoff cardinality");
        final Boolean followForeignKeys = scanBoolean(scanner, "fk");
        final Boolean isSafeMode = scanBoolean(scanner, "safe");

        final Map<Class<?>, File> optionalInputFiles = scanFilesByTypeOptionalSingle(scanner,
                ImmutableSet.<Class<?>>of(DbAnalyzerCorpus.class, Template.class));
        final File corpusFile = optionalInputFiles.get(DbAnalyzerCorpus.class);
        final File baseTemplateFile = optionalInputFiles.get(Template.class);

        generateCorpusBasedTemplates(broadcastCardinalityCutoff, followForeignKeys, isSafeMode, corpusFile,
                baseTemplateFile);
    }

    /**
     * Automatically generate templates from a given corpus.
     * @throws Throwable 
     */
    public void cmd_generate_templates(final Scanner scanner) throws Throwable {
        checkReport();
        checkDatabase();

        final Boolean followForeignKeys = scanBoolean(scanner, "fk");
        final Boolean isSafeMode = scanBoolean(scanner, "safe");

        final Map<Class<?>, File> optionalInputFiles = scanFilesByTypeOptionalSingle(scanner,
                ImmutableSet.<Class<?>>of(DbAnalyzerCorpus.class, Template.class));
        final File corpusFile = optionalInputFiles.get(DbAnalyzerCorpus.class);
        final File baseTemplateFile = optionalInputFiles.get(Template.class);

        generateCorpusBasedTemplates(null, followForeignKeys, isSafeMode, corpusFile, baseTemplateFile);
    }

    /**
     * Compare given templates.
     */
    public void cmd_compare_templates(Scanner scanner) throws PEException {
        final File baseTemplateFile = scanFile(scanner, "template 1");
        final File comparedTemplateFile = scanFile(scanner, "template 2");
        final File outputFile = scanFile(scanner);

        final Template baseTemplate = PEXmlUtils.unmarshalJAXB(baseTemplateFile, Template.class);
        final Template comparedTemplate = PEXmlUtils.unmarshalJAXB(comparedTemplateFile, Template.class);

        compareTemplates(baseTemplate, comparedTemplate, null, outputFile);
    }

    public void cmd_advanced_compare_templates(Scanner scanner) throws PEException {
        checkReport();
        checkDatabase();

        final File corpusFile = scanFile(scanner, "corpus file");
        final File baseTemplateFile = scanFile(scanner, "template 1");
        final File comparedTemplateFile = scanFile(scanner, "template 2");
        final File outputFile = scanFile(scanner);

        final DbAnalyzerCorpus corpus = loadCorpus(corpusFile);
        final CorpusStats corpusStats = new CorpusStats(corpus.getDescription(),
                this.options.getCorpusScaleFactor(), this.options.getDefaultStorageEngine());
        final Template baseTemplate = PEXmlUtils.unmarshalJAXB(baseTemplateFile, Template.class);
        final Template comparedTemplate = PEXmlUtils.unmarshalJAXB(comparedTemplateFile, Template.class);

        try {

            /* Generate initial "safe" templates for schema loading. */
            generateAllBroadcastTemplates();

            /* Analyze the static report. */
            final StatementAnalyzer analyzer = new StatementAnalyzer(this.options, corpusStats);
            loadSchema(analyzer);
            loadCorpusStats(analyzer, corpusStats);

            /* Analyze the corpus. */
            try (final FrequenciesSource frequencies = new FrequenciesSource(corpus)) {
                frequencies.analyze(analyzer);
            }

        } catch (final Throwable e) {
            clearTemplates();
            throw new PEException("Unable to analyze corpus '" + corpusFile.getAbsolutePath() + "'", e);
        }

        compareTemplates(baseTemplate, comparedTemplate, corpusStats, outputFile);
    }

    private void compareTemplates(final Template baseTemplate, final Template comparedTemplate,
            final CorpusStats corpusStats, final File outputFile) throws PEException {
        final Map<String, String> templateDiffs = compareTemplates(baseTemplate, comparedTemplate);
        final List<String> results = new ArrayList<String>(templateDiffs.size());
        for (final Map.Entry<String, String> item : templateDiffs.entrySet()) {
            final String tableName = item.getKey();
            final String itemDiff = item.getValue();

            final ColorStringBuilder entry = new ColorStringBuilder();
            if (corpusStats != null) {
                for (final Database db : getSelectedDatabases()) {
                    final TableStats table = corpusStats.findTable(
                            new QualifiedName(db.getName().concat(QualifiedName.PART_SEPARATOR).concat(tableName)));
                    entry.append(table.toString());
                }
            } else {
                entry.append(tableName);
            }

            results.add(entry.append(": ")
                    .append(itemDiff, (itemDiff.equals("MATCH") ? ConsoleColor.GREEN : ConsoleColor.DEFAULT))
                    .toString());
        }

        if (outputFile == null) {
            printCollection(results, System.out);
        } else {
            try (final PrintStream outputWriter = new PrintStream(outputFile)) {
                printCollection(results, outputWriter);
            } catch (final FileNotFoundException e) {
                throw new PEException("Could not open the output file '" + outputFile + "'");
            }
        }
    }

    public static Map<String, String> compareTemplates(final Template baseTemplate,
            final Template comparedTemplate) {
        final Comparator<TableTemplateType> orderByMatch = new Comparator<TableTemplateType>() {
            @Override
            public int compare(TableTemplateType a, TableTemplateType b) {
                if (a.getMatch().equals(".*")) {
                    return 0;
                }
                return a.getMatch().compareTo(b.getMatch());
            }
        };

        final Set<TableTemplateType> baseItems = new TreeSet<TableTemplateType>(orderByMatch);
        final Set<TableTemplateType> comparedItems = new TreeSet<TableTemplateType>(orderByMatch);

        baseItems.addAll(baseTemplate.getTabletemplate());
        comparedItems.addAll(comparedTemplate.getTabletemplate());

        final Set<TableTemplateType> addedItems = getDifference(comparedItems, baseItems, orderByMatch);
        final Set<TableTemplateType> removedItems = getDifference(baseItems, comparedItems, orderByMatch);
        final Set<TableTemplateType> intersectionItems = getIntersection(comparedItems, baseItems, orderByMatch);

        final String arrow = " -> ";
        final String columnSeparator = ", ";
        final Map<String, String> results = new TreeMap<String, String>();
        for (final TableTemplateType item : intersectionItems) {
            final TableTemplateType baseItem = getFromCollection(baseItems, item);
            final TableTemplateType comparedItem = getFromCollection(comparedItems, item);

            final ModelType baseModel = baseItem.getModel();
            final ModelType itemModel = comparedItem.getModel();

            if (!baseModel.equals(itemModel)) {
                results.put(item.getMatch(), baseModel.value().concat(arrow).concat(itemModel.value()));
            } else {
                if (baseModel.equals(ModelType.RANGE)) {
                    final List<String> baseColumns = baseItem.getColumn();
                    final List<String> itemColumns = comparedItem.getColumn();

                    if (!baseColumns.containsAll(itemColumns) || !itemColumns.containsAll(baseColumns)) {
                        results.put(item.getMatch(),
                                ("(" + ModelType.RANGE + ") <")
                                        .concat(StringUtils.join(baseColumns, columnSeparator)).concat(arrow)
                                        .concat(StringUtils.join(itemColumns, columnSeparator)).concat(">"));
                        continue;
                    }
                }
                results.put(item.getMatch(), "MATCH");
            }
        }

        for (final TableTemplateType item : addedItems) {
            results.put(item.getMatch(), "ADDED");
        }

        for (final TableTemplateType item : removedItems) {
            results.put(item.getMatch(), "REMOVED");
        }

        return results;
    }

    public void cmd_analyze_corpus(Scanner scanner) throws PEException {
        final File corpusFile = scanFile(scanner, "corpus file");

        final DbAnalyzerCorpus corpus = loadCorpus(corpusFile);

        final BasicStatsVisitor bsv = new BasicStatsVisitor();

        try {

            /* Generate "safe" templates for schema loading. */
            generateAllBroadcastTemplates();

            final StatementAnalyzer analyzer = new StatementAnalyzer(options, bsv);
            loadSchema(analyzer);

            try (final FrequenciesSource frequencies = new FrequenciesSource(corpus)) {
                frequencies.analyze(analyzer);
            }

            bsv.display(System.out);
        } catch (final Throwable e) {
            clearTemplates();
            throw new PEException("Unable to analyze corpus '" + corpusFile.getAbsolutePath() + "'", e);
        }
    }

    public void cmd_set_database(Scanner scanner) throws PEException {
        String database = scan(scanner, "database");

        while (database != null) {
            databases.add(database);
            database = scan(scanner);
        }

        println("... Databases set to '" + this.databasesToString() + "'");
    }

    private static class DynamicAnalyzerCallback extends AnalyzerCallback {
        @Override
        public void onResult(final AnalyzerResult bar) {
            @SuppressWarnings("resource")
            // This should be handled by the owner.
            final PrintStream output = getOutputStream();

            bar.printStatement(output);
            if (bar instanceof AnalyzerPlanningError) {
                output.println("... Analysis FAILED");
            }
            bar.printTables(output);
            if (bar instanceof AnalyzerPlanningResult) {
                ((AnalyzerPlanningResult) bar).printPlan(output);
            } else if (bar instanceof AnalyzerPlanningError) {
                ((AnalyzerPlanningError) bar).getFault().printStackTrace(output);
            } else if (bar instanceof AnalyzerPlanningNotice) {
                output.println(((AnalyzerPlanningNotice) bar).getMessage());
            }
        }
    }

    protected void writeToFileOrScreen(File file, String content) throws PEException {
        if (file != null) {
            PEFileUtils.writeToFile(file, content, true);
        } else {
            println(content);
        }
    }

    private void executeDynamicCmdOnAnalyzerSources(final Collection<? extends AnalyzerSource> sources,
            final File output, final AnalyzerCallback analyzerCallback) throws Throwable {
        try {
            final Emulator analyzer = getInitializedEmulator(analyzerCallback, output);

            for (final AnalyzerSource source : sources) {
                println("... Starting dynamic analysis of '" + source.getDescription() + "'");
                source.analyze(analyzer);
            }
            println("... Dynamic analysis complete");
        } catch (final Throwable e) {
            throw new PEException("Dynamic analysis has failed.", e);
        } finally {
            this.closeAnalyzerSources(sources);
            if (output != null) {
                analyzerCallback.close();
            }
        }
    }

    private void executeFrequenciesCmdOnFileSources(final Scanner scanner, final String sourceType)
            throws Throwable {
        final File corpusFile = scanFile(scanner, "corpus file");
        final Set<FileSource> sources = this.scanFileSources(scanner, sourceType);
        this.executeFrequenciesCmdOnAnalyzerSources(sources, corpusFile);
    }

    private void executeFrequenciesCmdOnAnalyzerSources(final Collection<? extends AnalyzerSource> sources,
            final File output) throws Throwable {
        final StatementCounter sc = getInitializedStatementCounter(output);

        try {
            for (final AnalyzerSource source : sources) {
                println("... Starting frequency analysis of '" + source.getDescription() + "'");
                source.analyze(sc);
            }
            println("... Frequency analysis complete");
        } catch (final Exception e) {
            throw new PEException("Frequency analysis has failed.", e);
        } finally {
            this.closeAnalyzerSources(sources);
            sc.close();
            clearTemplates();
        }
    }

    @SuppressWarnings("resource")
    private Set<FileSource> scanFileSources(final Scanner scanner, final String sourceType) throws Exception {

        /*
         * Scan given log files. There must be at least one.
         */
        final Set<FileSource> analyzerSources = new LinkedHashSet<FileSource>();
        try {
            File userLogFile = scanFile(scanner, sourceType + " log file");
            while (userLogFile != null) {
                analyzerSources.add(new FileSource(sourceType, userLogFile));
                userLogFile = scanFile(scanner, null);
            }

            return analyzerSources;
        } catch (final Exception e) {
            this.closeAnalyzerSources(analyzerSources);

            throw e;
        }
    }

    @SuppressWarnings("resource")
    private Set<JdbcSource> scanJdbcSources(final Scanner scanner, final String scanUrl, final String scanUser,
            final String scanPassword) throws Throwable {

        /*
         * Scan given log files. There must be at least one.
         */
        final Set<JdbcSource> analyzerSources = new LinkedHashSet<JdbcSource>();
        try {
            String logTable = scan(scanner, "log table");
            while (logTable != null) {
                analyzerSources.add(new JdbcSource(scanUrl, scanUser, scanPassword, logTable));
                logTable = scan(scanner, null);
            }

            return analyzerSources;
        } catch (final Throwable e) {
            this.closeAnalyzerSources(analyzerSources);

            throw e;
        }
    }

    private void closeAnalyzerSources(final Collection<? extends AnalyzerSource> sources) {
        for (final AnalyzerSource source : sources) {
            source.close();
        }
    }

    private void checkConnection() throws PEException {
        if (url == null) {
            throw new PEException("Not connected");
        }
    }

    private void checkTemplate() throws PEException {
        if (templates == null) {
            throw new PEException(
                    "No templates are available. You can load an existing templates or generate a new ones.");
        }
    }

    private Template checkTemplate(String name) throws PEException {
        checkTemplate();

        final Template t = templates.get(name);

        if (t != null) {
            return t;
        }

        throw new PEException("Template '" + name + "' is not available");
    }

    private void checkReport() throws PEException {
        if (report == null) {
            throw new PEException(
                    "You need a static report to proceed. You can load an existing report from file or generate a new one.");
        }
    }

    private void checkDatabase() throws PEException {
        if (databases.isEmpty()) {
            throw new PEException("Database list has not been configured");
        }
    }

    private String checkDatabase(String name) throws PEException {
        checkDatabase();

        for (final String db : databases) {
            if (name.equals(db)) {
                return db;
            }
        }
        throw new PEException("Database '" + name + "' is not in the list of databases");
    }

    private String databasesToString() {
        if (!databases.isEmpty()) {
            return StringUtils.join(databases, ", ");
        }

        return StringUtils.EMPTY;
    }

    private void addTemplate(final File templateFile) throws PEException {
        println("... Reading template from file '" + templateFile.getAbsolutePath() + "'");

        final Template template = PEXmlUtils.unmarshalJAXB(templateFile, Template.class);

        addTemplate(template);
    }

    private void reloadTemplates(final List<Template> reloadTemplates) throws PEException {
        clearTemplates();
        for (final Template template : reloadTemplates) {
            addTemplate(template);
        }
    }

    private void addTemplate(final Template template) throws PEException {
        checkDatabase(template.getName());

        templates.put(template.getName(), template);

        println("... Template '" + template.getName() + "' added to catalog");
    }

    private void clearDatabases() {
        println("... Removing current databases");
        databases.clear();
    }

    private void clearTemplates() {
        println("... Removing current templates");
        templates.clear();
    }

    private static <T extends TableTemplateType> T getFromCollection(final Collection<T> items, final T value) {
        for (final T item : items) {
            if (isMatch(item, value)) {
                return item;
            }
        }
        return null;
    }

    private static <T extends TableTemplateType> Set<T> getDifference(final Set<T> a, final Set<T> b,
            final Comparator<? super T> comparator) {
        final Set<T> difference = new TreeSet<T>(comparator);
        difference.addAll(a);
        for (final T aItem : a) {
            for (final T bItem : b) {
                if (isMatch(aItem, bItem)) {
                    difference.remove(aItem);
                }
            }
        }
        return difference;
    }

    private static <T extends TableTemplateType> Set<T> getIntersection(final Set<T> a, final Set<T> b,
            final Comparator<? super T> comparator) {
        final Set<T> intersection = new TreeSet<T>(comparator);
        intersection.addAll(a);
        final Set<T> difference = getDifference(intersection, b, comparator);
        intersection.removeAll(difference);
        return intersection;
    }

    private static <T extends TableTemplateType> boolean isMatch(final T a, final T b) {
        return a.getMatch().matches(b.getMatch()) || b.getMatch().matches(a.getMatch());
    }

    private static <T> void printCollection(final Collection<T> collection, final PrintStream output) {
        for (final T item : collection) {
            output.println(item.toString());
        }
    }

    private Map<QualifiedName, Integer> getRowCountsFromFile(final File input)
            throws FileNotFoundException, PEException {
        final Map<QualifiedName, Integer> rowCounts = new HashMap<QualifiedName, Integer>();
        try (final Scanner fileScanner = new Scanner(input)) {
            boolean isHeaderLine = true;
            final Integer[] metaColumnIndices = { -1, -1, -1 };
            while (fileScanner.hasNextLine()) {
                final String line = fileScanner.nextLine();
                try (@SuppressWarnings("resource")
                final Scanner lineScanner = new Scanner(line).useDelimiter("\t")) {
                    if (isHeaderLine) {
                        int headerColumnCounter = 0;
                        while (lineScanner.hasNext()) {
                            final String columnHeader = lineScanner.next();
                            if (columnHeader.equalsIgnoreCase("TABLE_SCHEMA")) {
                                metaColumnIndices[0] = headerColumnCounter;
                            } else if (columnHeader.equalsIgnoreCase("TABLE_NAME")) {
                                metaColumnIndices[1] = headerColumnCounter;
                            } else if (columnHeader.equalsIgnoreCase("TABLE_ROWS")) {
                                metaColumnIndices[2] = headerColumnCounter;
                            }
                            ++headerColumnCounter;
                        }

                        if (Collections.min(Arrays.asList(metaColumnIndices)) < 0) {
                            throw new PEException(
                                    "Could not find all required column ('TABLE_SCHEMA', 'TABLE_NAME' and 'TABLE_ROWS').");
                        }

                        isHeaderLine = false;
                    } else {

                        String databaseName = null;
                        String tableName = null;
                        int tableRowCount = 0;

                        int columnCounter = 0;
                        while (lineScanner.hasNext()) {
                            if (columnCounter == metaColumnIndices[0]) {
                                databaseName = lineScanner.next();
                            } else if (columnCounter == metaColumnIndices[1]) {
                                tableName = lineScanner.next();
                            } else if (columnCounter == metaColumnIndices[2]) {
                                try {
                                    tableRowCount = lineScanner.nextInt();
                                } catch (final InputMismatchException e) {
                                    tableRowCount = 0; // Treat NULL and invalid count values as zero.
                                }
                            } else {
                                lineScanner.next();
                            }
                            ++columnCounter;
                        }

                        final QualifiedName qualifiedTableName = new QualifiedName(
                                new UnqualifiedName(databaseName), new UnqualifiedName(tableName));
                        rowCounts.put(qualifiedTableName, tableRowCount);
                    }
                }
            }
        }

        return rowCounts;
    }

    private void loadSchema(final Analyzer analyzer) throws PEException {
        checkReport();
        checkDatabase();
        checkTemplate();

        for (final Database database : getSelectedDatabases()) {
            analyzer.loadSchema(checkTemplate(database.getName()), database, this.dbNative);
        }
    }

    private void loadCorpusStats(final Analyzer analyzer, final CorpusStats stats) throws PEException {
        checkReport();
        checkDatabase();

        analyzer.resolveSchemaObjects(getSelectedDatabases(), stats);
    }

    private List<Database> getSelectedDatabases() throws PEException {
        checkReport();
        checkDatabase();

        final List<Database> selectedDatabases = new ArrayList<Database>();
        for (final Database database : this.report.getDatabases().getDatabase()) {
            if (this.databases.contains(database.getName())) {
                selectedDatabases.add(database);
            }
        }

        return selectedDatabases;
    }

    private void generateAllBroadcastTemplates() throws PEException {
        reloadTemplates(AiTemplateBuilder.buildAllBroadcastTemplates(databases));
    }

    private void generateAllRandomTemplates() throws PEException {
        reloadTemplates(AiTemplateBuilder.buildAllRandomTemplates(databases));
    }

    private void generateBroadcastCutoffTemplates(final long broadcastCardinalityCutoff,
            final File baseTemplateFile) throws Exception {
        final CorpusStats corpusStats = new CorpusStats(EMPTY_FREQUENCY_CORPUS.getDescription(),
                this.options.getCorpusScaleFactor(), this.options.getDefaultStorageEngine());
        final Template baseTemplate = (baseTemplateFile != null)
                ? PEXmlUtils.unmarshalJAXB(baseTemplateFile, Template.class)
                : null;
        try {

            /* Generate "safe" templates for schema loading. */
            generateAllBroadcastTemplates();

            final StatementAnalyzer analyzer = new StatementAnalyzer(this.options, corpusStats);
            loadSchema(analyzer);
            loadCorpusStats(analyzer, corpusStats);
            final TemplateModelItem fallbackModel = this.options.getGeneratorDefaultFallbackModel();
            final boolean isRowWidthWeightingEnabled = this.options.isRowWidthWeightingEnabled();
            final AiTemplateBuilder templateBuilder = new AiTemplateBuilder(corpusStats, baseTemplate,
                    fallbackModel, this.getPrintStream());
            templateBuilder.setWildcardsEnabled(this.options.isTemplateWildcardsEnabled());
            templateBuilder.setVerbose(this.options.isVerboseGeneratorEnabled());
            templateBuilder.setForeignKeysAsJoins(this.options.isForeignKeysAsJoinsEnabled());
            templateBuilder.setUseIdentTuples(this.options.isIdentTuplesEnabled());
            templateBuilder.setUseSorts(this.options.isUsingSortsEnabled());
            templateBuilder.setUseWrites(this.options.isUsingWritesEnabled());

            reloadTemplates(templateBuilder.buildBroadcastCutoffTemplates(databases, broadcastCardinalityCutoff,
                    isRowWidthWeightingEnabled));

        } catch (final Throwable e) {
            clearTemplates();
            throw new PEException("Unable to generate templates.", e);
        }
    }

    private void generateCorpusBasedTemplates(final Long broadcastCardinalityCutoff,
            final boolean followForeignKeys, final boolean isSafeMode, final File corpusFile,
            final File baseTemplateFile) throws Throwable {

        /* If there is no corpus file, use an empty corpus instead. */
        final DbAnalyzerCorpus corpus = (corpusFile != null) ? loadCorpus(corpusFile) : EMPTY_FREQUENCY_CORPUS;
        final Template baseTemplate = (baseTemplateFile != null)
                ? PEXmlUtils.unmarshalJAXB(baseTemplateFile, Template.class)
                : null;

        try {

            /* Generate initial "safe" templates for schema loading. */
            generateAllBroadcastTemplates();

            /* Analyze the static report. */
            final CorpusStats corpusStats = new CorpusStats(corpus.getDescription(),
                    this.options.getCorpusScaleFactor(), this.options.getDefaultStorageEngine());
            final StatementAnalyzer analyzer = new StatementAnalyzer(this.options, corpusStats);
            loadSchema(analyzer);
            loadCorpusStats(analyzer, corpusStats);

            /* Analyze the corpus. */
            try (final FrequenciesSource frequencies = new FrequenciesSource(corpus)) {
                frequencies.analyze(analyzer);
            }

            /*
             * Build templates for selected databases.
             * Use a user defined Broadcast cardinality cutoff or try to
             * determine optimal distribution models automatically.
             */
            final TemplateModelItem fallbackModel = this.options.getGeneratorDefaultFallbackModel();
            final boolean isRowWidthWeightingEnabled = this.options.isRowWidthWeightingEnabled();
            final AiTemplateBuilder ai = new AiTemplateBuilder(corpusStats, baseTemplate, fallbackModel,
                    this.getPrintStream());
            ai.setWildcardsEnabled(this.options.isTemplateWildcardsEnabled());
            ai.setVerbose(this.options.isVerboseGeneratorEnabled());
            ai.setForeignKeysAsJoins(this.options.isForeignKeysAsJoinsEnabled());
            ai.setUseIdentTuples(this.options.isIdentTuplesEnabled());
            ai.setUseSorts(this.options.isUsingSortsEnabled());
            ai.setUseWrites(this.options.isUsingWritesEnabled());
            final List<Template> builtTemplates = ai.buildTemplates(this.databases, broadcastCardinalityCutoff,
                    followForeignKeys, isSafeMode, isRowWidthWeightingEnabled);

            /* Load the generated templates while trashing the old ones. */
            reloadTemplates(builtTemplates);

        } catch (final Throwable e) {
            clearTemplates();
            throw new PEException("Unable to generate templates.", e);
        }

        /* Validate the generated templates by loading the schema. */
        println("... Validating the templates");

        try {
            final Emulator validator = new Emulator(this.options, new AnalyzerCallback() {
                @Override
                public void onResult(AnalyzerResult bar) {
                    // Not needed.
                }
            });
            loadSchema(validator);
        } catch (final Exception e) {
            throw new PEException("Template validation has failed.", e);
        }
    }

    public static void main(String[] args) {
        try {
            new DVEAnalyzerCLI(args).start();
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

}