/*
* This software is subject to the terms of the Eclipse Public License v1.0
* Agreement, available at the following URL:
* http://www.eclipse.org/legal/epl-v10.html.
* You must accept the terms of that agreement to use this software.
*
* Copyright (c) 2002-2013 Pentaho Corporation..  All rights reserved.
*/

package mondrian.tui;

import mondrian.olap.Category;
import mondrian.olap.*;
import mondrian.olap.Connection;
import mondrian.olap.DriverManager;
import mondrian.olap.Hierarchy;
import mondrian.olap.fun.FunInfo;
import mondrian.olap.type.TypeUtil;
import mondrian.rolap.RolapConnectionProperties;
import mondrian.rolap.RolapCube;

import org.apache.log4j.Level;
import org.apache.log4j.*;

import org.eigenbase.util.property.Property;

import org.olap4j.CellSet;
import org.olap4j.OlapConnection;
import org.olap4j.OlapStatement;
import org.olap4j.OlapWrapper;
import org.olap4j.layout.RectangularCellSetFormatter;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.*;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Command line utility which reads and executes MDX commands.
 *
 * <p>TODO: describe how to use this class.</p>
 *
 * @author Richard Emberson
 */
public class CmdRunner {

    private static final String nl = Util.nl;

    private static boolean RELOAD_CONNECTION = true;
    private static String CATALOG_NAME = "FoodMart";

    private static final Map<Object, String> paraNameValues =
        new HashMap<Object, String>();

    private static String[][] commentDelim;
    private static char[] commentStartChars;
    private static boolean allowNestedComments;
    private static final boolean USE_OLAP4J = false;

    private final Options options;
    private long queryTime;
    private long totalQueryTime;
    private String filename;
    private String mdxCmd;
    private String mdxResult;
    private String error;
    private String stack;
    private String connectString;
    private Connection connection;
    private final PrintWriter out;

    static {
        setDefaultCommentState();
    }

    /**
     * Creates a <code>CmdRunner</code>.
     *
     * @param options Option set, or null to use default options
     * @param out Output writer, or null to use {@link System#out}.
     */
    public CmdRunner(Options options, PrintWriter out) {
        if (options == null) {
            options = new Options();
        }
        this.options = options;
        this.filename = null;
        this.mdxResult = null;
        this.error = null;
        this.queryTime = -1;
        if (out == null) {
            out = new PrintWriter(System.out);
        }
        this.out = out;
    }

    public void setTimeQueries(boolean timeQueries) {
        this.options.timeQueries = timeQueries;
    }

    public boolean getTimeQueries() {
        return options.timeQueries;
    }

    public long getQueryTime() {
        return queryTime;
    }

    public long getTotalQueryTime() {
        return totalQueryTime;
    }

    public void noCubeCaching() {
        Cube[] cubes = getCubes();
        for (Cube cube : cubes) {
            RolapCube rcube = (RolapCube) cube;
            rcube.setCacheAggregations(false);
        }
    }

    void setError(String s) {
        this.error = s;
    }

    void setError(Throwable t) {
        this.error = formatError(t);
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        t.printStackTrace(pw);
        pw.flush();
        this.stack = sw.toString();
    }

    void clearError() {
        this.error = null;
        this.stack = null;
    }

    private String formatError(Throwable mex) {
        String message = mex.getMessage();
        if (message == null) {
            message = mex.toString();
        }
        if (mex.getCause() != null && mex.getCause() != mex) {
            message = message + nl + formatError(mex.getCause());
        }
        return message;
    }

    public static void listPropertyNames(StringBuilder buf) {
        PropertyInfo propertyInfo =
                new PropertyInfo(MondrianProperties.instance());
        for (int i = 0; i < propertyInfo.size(); i++) {
            buf.append(propertyInfo.getProperty(i).getPath());
            buf.append(nl);
        }
    }

    public static void listPropertiesAll(StringBuilder buf) {
        PropertyInfo propertyInfo =
                new PropertyInfo(MondrianProperties.instance());
        for (int i = 0; i < propertyInfo.size(); i++) {
            String propertyName = propertyInfo.getPropertyName(i);
            String propertyValue = propertyInfo.getProperty(i).getString();
            buf.append(propertyName);
            buf.append('=');
            buf.append(propertyValue);
            buf.append(nl);
        }
    }

    /**
     * Returns the value of a property, or null if it is not set.
     */
    private static String getPropertyValue(String propertyName) {
        final Property property = PropertyInfo.lookupProperty(
            MondrianProperties.instance(),
            propertyName);
        return property.isSet()
            ? property.getString()
            : null;
    }

    public static void listProperty(String propertyName, StringBuilder buf) {
        buf.append(getPropertyValue(propertyName));
    }

    public static boolean isProperty(String propertyName) {
        final Property property = PropertyInfo.lookupProperty(
            MondrianProperties.instance(),
            propertyName);
        return property != null;
    }

    public static boolean setProperty(String name, String value) {
        final Property property = PropertyInfo.lookupProperty(
            MondrianProperties.instance(),
            name);
        String oldValue = property.getString();
        if (! Util.equals(oldValue, value)) {
            property.setString(value);
            return true;
        } else {
            return false;
        }
    }

    public void loadParameters(Query query) {
        Parameter[] params = query.getParameters();
        for (Parameter param : params) {
            loadParameter(query, param);
        }
    }

    /**
     * Looks up the definition of a property with a given name.
     */
    private static class PropertyInfo {
        private final List<Property> propertyList = new ArrayList<Property>();
        private final List<String> propertyNameList = new ArrayList<String>();

        PropertyInfo(MondrianProperties properties) {
            final Class<? extends Object> clazz = properties.getClass();
            final Field[] fields = clazz.getFields();
            for (Field field : fields) {
                if (!Modifier.isPublic(field.getModifiers())
                    || Modifier.isStatic(field.getModifiers())
                    || !Property.class.isAssignableFrom(
                        field.getType()))
                {
                    continue;
                }
                final Property property;
                try {
                    property = (Property) field.get(properties);
                } catch (IllegalAccessException e) {
                    continue;
                }
                propertyList.add(property);
                propertyNameList.add(field.getName());
            }
        }

        public int size() {
            return propertyList.size();
        }

        public Property getProperty(int i) {
            return propertyList.get(i);
        }

        public String getPropertyName(int i) {
            return propertyNameList.get(i);
        }

        /**
         * Looks up the definition of a property with a given name.
         */
        public static Property lookupProperty(
            MondrianProperties properties,
            String propertyName)
        {
            final Class<? extends Object> clazz = properties.getClass();
            final Field field;
            try {
                field = clazz.getField(propertyName);
            } catch (NoSuchFieldException e) {
                return null;
            }
            if (!Modifier.isPublic(field.getModifiers())
                || Modifier.isStatic(field.getModifiers())
                || !Property.class.isAssignableFrom(field.getType()))
            {
                return null;
            }
            try {
                return (Property) field.get(properties);
            } catch (IllegalAccessException e) {
                return null;
            }
        }
    }

    private static class Expr {
        enum Type {
            STRING,
            NUMERIC,
            MEMBER
        }

        final Object value;
        final Type type;
        Expr(Object value, Type type) {
            this.value = value;
            this.type = type;
        }
    }

    public void loadParameter(Query query, Parameter param) {
        int category = TypeUtil.typeToCategory(param.getType());
        String name = param.getName();
        String value = CmdRunner.paraNameValues.get(name);
        debug("loadParameter: name=" + name + ", value=" + value);
        if (value == null) {
            return;
        }
        Expr expr = parseParameter(value);
        if  (expr == null) {
            return;
        }
        Expr.Type type = expr.type;
        // found the parameter with the given name in the query
        switch (category) {
        case Category.Numeric:
            if (type != Expr.Type.NUMERIC) {
                String msg =
                    "For parameter named \""
                    + name
                    + "\" of Catetory.Numeric, "
                    + "the value was type \""
                    + type
                    + "\"";
                throw new IllegalArgumentException(msg);
            }
            break;
        case Category.String:
            if (type != Expr.Type.STRING) {
                String msg =
                    "For parameter named \""
                    + name
                    + "\" of Catetory.String, "
                    + "the value was type \""
                    + type
                    + "\"";
                throw new IllegalArgumentException(msg);
            }
            break;

        case Category.Member:
            if (type != Expr.Type.MEMBER) {
                String msg = "For parameter named \""
                        + name
                        + "\" of Catetory.Member, "
                        + "the value was type \""
                        + type
                        + "\"";
                throw new IllegalArgumentException(msg);
            }
            break;

        default:
            throw Util.newInternal("unexpected category " + category);
        }
        query.setParameter(param.getName(), String.valueOf(expr.value));
    }

    static NumberFormat nf = NumberFormat.getInstance();

    // this is taken from JPivot
    public Expr parseParameter(String value) {
        // is it a String (enclose in double or single quotes ?
        String trimmed = value.trim();
        int len = trimmed.length();
        if (trimmed.charAt(0) == '"' && trimmed.charAt(len - 1) == '"') {
            debug("parseParameter. STRING_TYPE: " + trimmed);
            return new Expr(
                trimmed.substring(1, trimmed.length() - 1),
                Expr.Type.STRING);
        }
        if (trimmed.charAt(0) == '\'' && trimmed.charAt(len - 1) == '\'') {
            debug("parseParameter. STRING_TYPE: " + trimmed);
            return new Expr(
                trimmed.substring(1, trimmed.length() - 1),
                Expr.Type.STRING);
        }

        // is it a Number ?
        Number number = null;
        try {
            number = nf.parse(trimmed);
        } catch (ParseException pex) {
            // nothing to do, should be member
        }
        if (number != null) {
            debug("parseParameter. NUMERIC_TYPE: " + number);
            return new Expr(number, Expr.Type.NUMERIC);
        }

        debug("parseParameter. MEMBER_TYPE: " + trimmed);
        Query query = this.connection.parseQuery(this.mdxCmd);
        // dont have to execute
        //this.connection.execute(query);

        // assume member, dimension, hierarchy, level
        OlapElement element = Util.lookup(query, Util.parseIdentifier(trimmed));

        debug(
            "parseParameter. exp="
            + ((element == null) ? "null" : element.getClass().getName()));

        if (element instanceof Member) {
            Member member = (Member) element;
            return new Expr(member, Expr.Type.MEMBER);
        } else if (element instanceof mondrian.olap.Level) {
            mondrian.olap.Level level = (mondrian.olap.Level) element;
            return new Expr(level, Expr.Type.MEMBER);
        } else if (element instanceof Hierarchy) {
            Hierarchy hier = (Hierarchy) element;
            return new Expr(hier, Expr.Type.MEMBER);
        } else if (element instanceof Dimension) {
            Dimension dim = (Dimension) element;
            return new Expr(dim, Expr.Type.MEMBER);
        }
        return null;
    }

    public static void listParameterNameValues(StringBuilder buf) {
        for (Map.Entry<Object, String> e
            : CmdRunner.paraNameValues.entrySet())
        {
            buf.append(e.getKey());
            buf.append('=');
            buf.append(e.getValue());
            buf.append(nl);
        }
    }

    public static void listParam(String name, StringBuilder buf) {
        String v = CmdRunner.paraNameValues.get(name);
        buf.append(v);
    }

    public static boolean isParam(String name) {
        String v = CmdRunner.paraNameValues.get(name);
        return (v != null);
    }

    public static void setParameter(String name, String value) {
        if (name == null) {
            CmdRunner.paraNameValues.clear();
        } else {
            if (value == null) {
                CmdRunner.paraNameValues.remove(name);
            } else {
                CmdRunner.paraNameValues.put(name, value);
            }
        }
    }

    /////////////////////////////////////////////////////////////////////////
    //
    // cubes
    //
    public Cube[] getCubes() {
        Connection conn = getConnection();
        return conn.getSchemaReader().withLocus().getCubes();
    }

    public Cube getCube(String name) {
        Cube[] cubes = getCubes();
        for (Cube cube : cubes) {
            if (cube.getName().equals(name)) {
                return cube;
            }
        }
        return null;
    }

    public void listCubeName(StringBuilder buf) {
        Cube[] cubes = getCubes();
        for (Cube cube : cubes) {
            buf.append(cube.getName());
            buf.append(nl);
        }
    }

    public void listCubeAttribues(String name, StringBuilder buf) {
        Cube cube = getCube(name);
        if (cube == null) {
            buf.append("No cube found with name \"");
            buf.append(name);
            buf.append("\"");
        } else {
            RolapCube rcube = (RolapCube) cube;
            buf.append("facttable=");
            buf.append(rcube.getStar().getFactTable().getAlias());
            buf.append(nl);
            buf.append("caching=");
            buf.append(rcube.isCacheAggregations());
            buf.append(nl);
        }
    }

    public void executeCubeCommand(
        String cubename,
        String command,
        StringBuilder buf)
    {
        Cube cube = getCube(cubename);
        if (cube == null) {
            buf.append("No cube found with name \"");
            buf.append(cubename);
            buf.append("\"");
        } else {
            if (command.equals("clearCache")) {
                RolapCube rcube = (RolapCube) cube;
                rcube.clearCachedAggregations();
            } else {
                buf.append("For cube \"");
                buf.append(cubename);
                buf.append("\" there is no command \"");
                buf.append(command);
                buf.append("\"");
            }
        }
    }

    public void setCubeAttribute(
        String cubename,
        String name,
        String value,
        StringBuilder buf)
    {
        Cube cube = getCube(cubename);
        if (cube == null) {
            buf.append("No cube found with name \"");
            buf.append(cubename);
            buf.append("\"");
        } else {
            if (name.equals("caching")) {
                RolapCube rcube = (RolapCube) cube;
                boolean isCache = Boolean.valueOf(value);
                rcube.setCacheAggregations(isCache);
            } else {
                buf.append("For cube \"");
                buf.append(cubename);
                buf.append("\" there is no attribute \"");
                buf.append(name);
                buf.append("\"");
            }
        }
    }
    //
    /////////////////////////////////////////////////////////////////////////

    /**
     * Executes a query and returns the result as a string.
     *
     * @param queryString MDX query text
     * @return result String
     */
    public String execute(String queryString) {
        if (USE_OLAP4J) {
            return runQuery(
                queryString,
                new Util.Functor1<String, CellSet>() {
                    public String apply(CellSet param) {
                        StringWriter stringWriter = new StringWriter();
                        PrintWriter printWriter = new PrintWriter(stringWriter);
                        new RectangularCellSetFormatter(false)
                            .format(param, printWriter);
                        printWriter.flush();
                        return stringWriter.toString();
                    }
                });
        }
        Result result = runQuery(queryString, true);
        if (this.options.highCardResults) {
            return highCardToString(result);
        } else {
            return toString(result);
        }
    }

    /**
     * Executes a query and returns the result.
     *
     * @param queryString MDX query text
     * @return a {@link Result} object
     */
    public Result runQuery(String queryString, boolean loadParams) {
        debug("CmdRunner.runQuery: TOP");
        Result result = null;
        long start = System.currentTimeMillis();
        try {
            this.connection = getConnection();
            debug("CmdRunner.runQuery: AFTER getConnection");
            Query query = this.connection.parseQuery(queryString);
            debug("CmdRunner.runQuery: AFTER parseQuery");
            if (loadParams) {
                loadParameters(query);
            }
            start = System.currentTimeMillis();
            result = this.connection.execute(query);
        } finally {
            queryTime = (System.currentTimeMillis() - start);
            totalQueryTime += queryTime;
            debug("CmdRunner.runQuery: BOTTOM");
        }
        return result;
    }

    /**
     * Executes a query and processes the result using a callback.
     *
     * @param queryString MDX query text
     */
    public <T> T runQuery(String queryString, Util.Functor1<T, CellSet> f) {
        long start = System.currentTimeMillis();
        OlapConnection connection = null;
        OlapStatement statement = null;
        CellSet cellSet = null;
        try {
            connection = getOlapConnection();
            statement = connection.createStatement();
            debug("CmdRunner.runQuery: AFTER createStatement");
            start = System.currentTimeMillis();
            cellSet = statement.executeOlapQuery(queryString);
            return f.apply(cellSet);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            queryTime = (System.currentTimeMillis() - start);
            totalQueryTime += queryTime;
            debug("CmdRunner.runQuery: BOTTOM");
            Util.close(cellSet, statement, connection);
        }
    }

    /**
     * Converts a {@link Result} object to a string
     *
     * @return String version of mondrian Result object.
     */
    public String toString(Result result) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        result.print(pw);
        pw.flush();
        return sw.toString();
    }
    /**
     * Converts a {@link Result} object to a string printing to standard
     * output directly, without buffering.
     *
     * @return null String since output is dump directly to stdout.
     */
    public String highCardToString(Result result) {
        result.print(new PrintWriter(System.out, true));
        return null;
    }


    public void makeConnectString() {
        String connectString = CmdRunner.getConnectStringProperty();
        debug("CmdRunner.makeConnectString: connectString=" + connectString);

        Util.PropertyList connectProperties;
        if (connectString == null || connectString.equals("")) {
            // create new and add provider
            connectProperties = new Util.PropertyList();
            connectProperties.put(
                RolapConnectionProperties.Provider.name(),
                "mondrian");
        } else {
            // load with existing connect string
            connectProperties = Util.parseConnectString(connectString);
        }

        // override jdbc url
        String jdbcURL = CmdRunner.getJdbcURLProperty();

        debug("CmdRunner.makeConnectString: jdbcURL=" + jdbcURL);

        if (jdbcURL != null) {
            // add jdbc url to connect string
            connectProperties.put(
                RolapConnectionProperties.Jdbc.name(),
                jdbcURL);
        }

        // override jdbc drivers
        String jdbcDrivers = CmdRunner.getJdbcDriversProperty();

        debug("CmdRunner.makeConnectString: jdbcDrivers=" + jdbcDrivers);
        if (jdbcDrivers != null) {
            // add jdbc drivers to connect string
            connectProperties.put(
                RolapConnectionProperties.JdbcDrivers.name(),
                jdbcDrivers);
        }

        // override catalog url
        String catalogURL = CmdRunner.getCatalogURLProperty();

        debug("CmdRunner.makeConnectString: catalogURL=" + catalogURL);

        if (catalogURL != null) {
            // add catalog url to connect string
            connectProperties.put(
                RolapConnectionProperties.Catalog.name(),
                catalogURL);
        }

        // override JDBC user
        String jdbcUser = CmdRunner.getJdbcUserProperty();

        debug("CmdRunner.makeConnectString: jdbcUser=" + jdbcUser);

        if (jdbcUser != null) {
            // add user to connect string
            connectProperties.put(
                RolapConnectionProperties.JdbcUser.name(),
                jdbcUser);
        }

        // override JDBC password
        String jdbcPassword = CmdRunner.getJdbcPasswordProperty();

        debug("CmdRunner.makeConnectString: jdbcPassword=" + jdbcPassword);

        if (jdbcPassword != null) {
            // add password to connect string
            connectProperties.put(
                RolapConnectionProperties.JdbcPassword.name(),
                jdbcPassword);
        }

        if (options.roleName != null) {
            connectProperties.put(
                RolapConnectionProperties.Role.name(),
                options.roleName);
        }

        debug(
            "CmdRunner.makeConnectString: connectProperties="
            + connectProperties);

        this.connectString = connectProperties.toString();
    }

    /**
     * Gets a connection to Mondrian.
     *
     * @return Mondrian {@link Connection}
     */
    public Connection getConnection() {
        return getConnection(CmdRunner.RELOAD_CONNECTION);
    }

    /**
     * Gets a Mondrian connection, creating a new one if fresh is true.
     *
     * @return mondrian Connection.
     */
    public synchronized Connection getConnection(boolean fresh) {
        // FIXME: fresh is currently ignored.
        if (this.connectString == null) {
            makeConnectString();
        }
        if (this.connection == null) {
            this.connection =
                DriverManager.getConnection(this.connectString, null);
        }
        return this.connection;
    }

    /**
     * Gets an olap4j connection, creating a new one if fresh is true.
     *
     * @return mondrian Connection.
     */
    public synchronized OlapConnection getOlapConnection() throws SQLException {
        if (this.connectString == null) {
            makeConnectString();
        }
        final String olapConnectString = "jdbc:mondrian:" + connectString;
        final java.sql.Connection jdbcConnection =
            java.sql.DriverManager.getConnection(olapConnectString);
        // Cast to OlapWrapper lets code work on JDK1.5, before java.sql.Wrapper
        //noinspection RedundantCast
        return ((OlapWrapper) jdbcConnection).unwrap(OlapConnection.class);
    }

    public String getConnectString() {
        return getConnectString(CmdRunner.RELOAD_CONNECTION);
    }

    public synchronized String getConnectString(boolean fresh) {
        if (this.connectString == null) {
            makeConnectString();
        }
        return this.connectString;
    }

    /////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////
    //
    // static methods
    //
    /////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////

    protected void debug(String msg) {
        if (options.debug) {
            out.println(msg);
        }
    }

    /////////////////////////////////////////////////////////////////////////
    // properties
    /////////////////////////////////////////////////////////////////////////
    protected static String getConnectStringProperty() {
        return MondrianProperties.instance().TestConnectString.get();
    }
    protected static String getJdbcURLProperty() {
        return MondrianProperties.instance().FoodmartJdbcURL.get();
    }

    protected static String getJdbcUserProperty() {
        return MondrianProperties.instance().TestJdbcUser.get();
    }

    protected static String getJdbcPasswordProperty() {
        return MondrianProperties.instance().TestJdbcPassword.get();
    }
    protected static String getCatalogURLProperty() {
        return MondrianProperties.instance().CatalogURL.get();
    }
    protected static String getJdbcDriversProperty() {
        return MondrianProperties.instance().JdbcDrivers.get();
    }

    /////////////////////////////////////////////////////////////////////////
    // command loop
    /////////////////////////////////////////////////////////////////////////

    protected void commandLoop(boolean interactive) throws IOException {
        commandLoop(
            new BufferedReader(
                new InputStreamReader(System.in)),
                interactive);
    }

    protected void commandLoop(File file) throws IOException {
        // If we open a stream, then we close it.
        FileReader in = new FileReader(file);
        try {
            commandLoop(new BufferedReader(in), false);
        } finally {
            try {
                in.close();
            } catch (Exception ex) {
                // ignore
            }
        }
    }

    protected void commandLoop(
        String mdxCmd,
        boolean interactive)
        throws IOException
    {
        StringReader is = new StringReader(mdxCmd);
        commandLoop(is, interactive);
    }

    private static final String COMMAND_PROMPT_START = "> ";
    private static final String COMMAND_PROMPT_MID = "? ";

    /**
     * The Command Loop where lines are read from the InputStream and
     * interpreted. If interactive then prompts are printed.
     *
     * @param in Input reader (preferably buffered)
     * @param interactive Whether the session is interactive
     */
    protected void commandLoop(Reader in, boolean interactive) {
        StringBuilder buf = new StringBuilder(2048);
        boolean inMdxCmd = false;
        String resultString = null;

        for (;;) {
            if (resultString != null) {
                printResults(resultString);
                printQueryTime();
                resultString = null;
                buf.setLength(0);
            } else if (interactive && (error != null)) {
                printResults(error);
                printQueryTime();
            }
            if (interactive) {
                if (inMdxCmd) {
                    out.print(COMMAND_PROMPT_MID);
                } else {
                    out.print(COMMAND_PROMPT_START);
                }
                out.flush();
            }
            if (!inMdxCmd) {
                buf.setLength(0);
            }
            String line;
            try {
                line = readLine(in, inMdxCmd);
            } catch (IOException e) {
                throw new RuntimeException(
                    "Exception while reading command line", e);
            }
            if (line != null) {
                line = line.trim();
            }
            debug("line=" + line);

            if (! inMdxCmd) {
                // If not in the middle of reading an mdx query and
                // we reach end of file on the stream, then we are over.
                if (line == null) {
                    return;
                }
            }

            // If not reading an mdx query, then check if the line is a
            // user command.
            if (! inMdxCmd) {
                String cmd = line;
                if (cmd.startsWith("help")) {
                    resultString = executeHelp(cmd);
                } else if (cmd.startsWith("set")) {
                    resultString = executeSet(cmd);
                } else if (cmd.startsWith("log")) {
                    resultString = executeLog(cmd);
                } else if (cmd.startsWith("file")) {
                    resultString = executeFile(cmd);
                } else if (cmd.startsWith("list")) {
                    resultString = executeList(cmd);
                } else if (cmd.startsWith("func")) {
                    resultString = executeFunc(cmd);
                } else if (cmd.startsWith("param")) {
                    resultString = executeParam(cmd);
                } else if (cmd.startsWith("cube")) {
                    resultString = executeCube(cmd);
                } else if (cmd.startsWith("error")) {
                    resultString = executeError(cmd);
                } else if (cmd.startsWith("echo")) {
                    resultString = executeEcho(cmd);
                } else if (cmd.startsWith("expr")) {
                    resultString = executeExpr(cmd);
                } else if (cmd.equals("=")) {
                    resultString = reExecuteMdxCmd();
                } else if (cmd.startsWith("exit")) {
                    break;
                }
                if (resultString != null) {
                    inMdxCmd = false;
                    continue;
                }
            }

            // Are we ready to execute an mdx query.
            if ((line == null)
                || ((line.length() == 1)
                    && ((line.charAt(0) == EXECUTE_CHAR)
                        || (line.charAt(0) == CANCEL_CHAR))))
            {
                // If EXECUTE_CHAR, then execute, otherwise its the
                // CANCEL_CHAR and simply empty buffer.
                if ((line == null) || (line.charAt(0) == EXECUTE_CHAR)) {
                    String mdxCmd = buf.toString().trim();
                    debug("mdxCmd=\"" + mdxCmd + "\"");
                    resultString = executeMdxCmd(mdxCmd);
                }

                inMdxCmd = false;

            } else if (line.length() > 0) {
                // OK, just add the line to the mdx query we are building.
                inMdxCmd = true;

                if (line.endsWith(SEMI_COLON_STRING)) {
                    // Remove the ';' character.
                    buf.append(line.substring(0, line.length() - 1));
                    String mdxCmd = buf.toString().trim();
                    debug("mdxCmd=\"" + mdxCmd + "\"");
                    resultString = executeMdxCmd(mdxCmd);
                    inMdxCmd = false;
                } else {
                    buf.append(line);
                    // add carriage return so that query keeps formatting
                    buf.append(nl);
                }
            }
        }
    }

    protected void printResults(String resultString) {
        if (resultString != null) {
            resultString = resultString.trim();
            if (resultString.length() > 0) {
                out.println(resultString);
                out.flush();
            }
        }
    }
    protected void printQueryTime() {
        if (options.timeQueries && (queryTime != -1)) {
            out.println("time[" + queryTime +  "ms]");
            out.flush();
            queryTime = -1;
        }
    }

    /**
     * Gather up a line ending in '\n' or EOF.
     * Returns null if at EOF.
     * Strip out comments. If a comment character appears within a
     * string then its not a comment. Strings are defined with "\"" or
     * "'" characters. Also, a string can span more than one line (a
     * nice little complication). So, if we read a string, then we consume
     * the whole string as part of the "line" returned,
     * including EOL characters.
     * If an escape character is seen '\\', then it and the next character
     * is added to the line regardless of what the next character is.
     */
    protected static String readLine(
        Reader reader,
        boolean inMdxCmd)
        throws IOException
    {
        StringBuilder buf = new StringBuilder(128);
        StringBuilder line = new StringBuilder(128);
        int offset;
        int i = getLine(reader, line);
        boolean inName = false;

        for (offset = 0; offset < line.length(); offset++) {
            char c = line.charAt(offset);

            if (c == ESCAPE_CHAR) {
                buf.append(ESCAPE_CHAR);
                buf.append(line.charAt(++offset));
            } else if (!inName
                       && ((c == STRING_CHAR_1) || (c == STRING_CHAR_2)))
            {
                i = readString(reader, line, offset, buf, i);
                offset = 0;
            } else {
                int commentType=-1;

                if (c == BRACKET_START) {
                    inName = true;
                } else if (c == BRACKET_END) {
                    inName = false;
                } else if (! inName) {
                    // check if we have the start of a comment block
                    // check if we have the start of a comment block
                    for (int x = 0; x < commentDelim.length; x++) {
                        if (c != commentStartChars[x]) {
                            continue;
                        }
                        String startComment = commentDelim[x][0];
                        boolean foundCommentStart = true;
                        for (int j = 1;
                            j + offset < line.length()
                            && j < startComment.length();
                            j++)
                        {
                            if (line.charAt(j + offset)
                                != startComment.charAt(j))
                            {
                                foundCommentStart = false;
                            }
                        }

                        if (foundCommentStart) {
                            if (x == 0) {
                                // A '#' must be the first character on a line
                                if (offset == 0) {
                                    commentType = x;
                                    break;
                                }
                            } else {
                                commentType = x;
                                break;
                            }
                        }
                    }
                }

                // -1 means no comment
                if (commentType == -1) {
                    buf.append(c);
                } else {
                    // check for comment to end of line comment
                    if (commentDelim[commentType][1] == null) {
                        break;
                    } else {
                        // handle delimited comment block
                        i = readBlock(
                            reader, line, offset,
                            commentDelim[commentType][0],
                            commentDelim[commentType][1],
                            false, false, buf, i);
                        offset = 0;
                    }
                }
            }
        }

        if (i == -1 && buf.length() == 0) {
            return null;
        } else {
            return buf.toString();
        }
    }

   /**
     * Read the next line of input.  Return the terminating character,
     * -1 for end of file, or \n or \r.  Add \n and \r to the end of the
     * buffer to be included in strings and comment blocks.
     */
    protected static int getLine(
        Reader reader,
        StringBuilder line)
        throws IOException
    {
        line.setLength(0);
        for (;;) {
            int i = reader.read();

            if (i == -1) {
                return i;
            }

            line.append((char)i);

            if (i == '\n' || i == '\r') {
                return i;
            }
        }
    }

    /**
     * Start of a string, read all of it even if it spans
     * more than one line adding each line's <cr> to the
     * buffer.
     */
    protected static int readString(
        Reader reader,
        StringBuilder line,
        int offset,
        StringBuilder buf,
        int i)
        throws IOException
    {
        String delim = line.substring(offset, offset + 1);
        return readBlock(
            reader, line, offset, delim, delim, true, true, buf, i);
    }

    /**
     * Start of a delimted block, read all of it even if it spans
     * more than one line adding each line's <cr> to the
     * buffer.
     *
     * A delimited block is a delimited comment (/\* ... *\/), or a string.
     */
    protected static int readBlock(
        Reader reader,
        StringBuilder line,
        int offset,
        final String startDelim,
        final String endDelim,
        final boolean allowEscape,
        final boolean addToBuf,
        StringBuilder buf,
        int i)
        throws IOException
    {
        int depth = 1;
        if (addToBuf) {
            buf.append(startDelim);
        }
        offset += startDelim.length();

        for (;;) {
            // see if we are at the end of the block
            if (line.substring(offset).startsWith(endDelim)) {
                if (addToBuf) {
                    buf.append(endDelim);
                }
                offset += endDelim.length();
                if (--depth == 0) {
                     break;
                }
            // check for nested block
            } else if (allowNestedComments
                       && line.substring(offset).startsWith(startDelim))
            {
                if (addToBuf) {
                    buf.append(startDelim);
                }
                offset += startDelim.length();
                depth++;
            } else if (offset < line.length()) {
                // not at the end of line, so eat the next char
                char c = line.charAt(offset++);
                if (allowEscape && c == ESCAPE_CHAR) {
                    if (addToBuf) {
                        buf.append(ESCAPE_CHAR);
                    }

                    if (offset < line.length()) {
                        if (addToBuf) {
                            buf.append(line.charAt(offset));
                        }
                        offset++;
                    }
                } else if (addToBuf) {
                    buf.append(c);
                }
            } else {
                // finished a line; read in the next one and continue
                if (i == -1) {
                    break;
                }
                i = getLine(reader, line);

                // line will always contain EOL marker at least, unless at EOF
                offset = 0;
                if (line.length() == 0) {
                    break;
                }
            }
        }

        // remove to the end of the string, so caller starts at offset 0
        if (offset > 0) {
            line.delete(0, offset - 1);
        }

        return i;
    }

    /////////////////////////////////////////////////////////////////////////
    // xmla file
    /////////////////////////////////////////////////////////////////////////

    /**
     * This is called to process a file containing XMLA as the contents
     * of SOAP xml.
     *
     */
    protected void processSoapXmla(
        File file,
        int validateXmlaResponse)
        throws Exception
    {
        String catalogURL = CmdRunner.getCatalogURLProperty();
        Map<String, String> catalogNameUrls = new HashMap<String, String>();
        catalogNameUrls.put(CATALOG_NAME, catalogURL);

        long start = System.currentTimeMillis();

        byte[] bytes = null;
        try {
            bytes = XmlaSupport.processSoapXmla(
                file,
                getConnectString(),
                catalogNameUrls,
                null);
        } finally {
            queryTime = (System.currentTimeMillis() - start);
            totalQueryTime += queryTime;
        }

        String response = new String(bytes);
        out.println(response);

        switch (validateXmlaResponse) {
        case VALIDATE_NONE:
            break;
        case VALIDATE_TRANSFORM:
            XmlaSupport.validateSchemaSoapXmla(bytes);
            out.println("XML Data is Valid");
            break;
        case VALIDATE_XPATH:
            XmlaSupport.validateSoapXmlaUsingXpath(bytes);
            out.println("XML Data is Valid");
            break;
        }
    }

    /**
     * This is called to process a file containing XMLA xml.
     *
     */
    protected void processXmla(
        File file,
        int validateXmlaResponce)
        throws Exception
    {
        String catalogURL = CmdRunner.getCatalogURLProperty();
        Map<String, String> catalogNameUrls = new HashMap<String, String>();
        catalogNameUrls.put(CATALOG_NAME, catalogURL);

        long start = System.currentTimeMillis();

        byte[] bytes = null;
        try {
            bytes = XmlaSupport.processXmla(
                file,
                getConnectString(),
                catalogNameUrls);
        } finally {
            queryTime = (System.currentTimeMillis() - start);
            totalQueryTime += queryTime;
        }

        String response = new String(bytes);
        out.println(response);

        switch (validateXmlaResponce) {
        case VALIDATE_NONE:
            break;
        case VALIDATE_TRANSFORM:
            XmlaSupport.validateSchemaXmla(bytes);
            out.println("XML Data is Valid");
            break;
        case VALIDATE_XPATH:
            XmlaSupport.validateXmlaUsingXpath(bytes);
            out.println("XML Data is Valid");
            break;
        }
    }

    /////////////////////////////////////////////////////////////////////////
    // user commands and help messages
    /////////////////////////////////////////////////////////////////////////
    private static final String INDENT = "  ";

    private static final int UNKNOWN_CMD        = 0x0000;
    private static final int HELP_CMD           = 0x0001;
    private static final int SET_CMD            = 0x0002;
    private static final int LOG_CMD            = 0x0004;
    private static final int FILE_CMD           = 0x0008;
    private static final int LIST_CMD           = 0x0010;
    private static final int MDX_CMD            = 0x0020;
    private static final int FUNC_CMD           = 0x0040;
    private static final int PARAM_CMD          = 0x0080;
    private static final int CUBE_CMD           = 0x0100;
    private static final int ERROR_CMD          = 0x0200;
    private static final int ECHO_CMD           = 0x0400;
    private static final int EXPR_CMD           = 0x0800;
    private static final int EXIT_CMD           = 0x1000;

    private static final int ALL_CMD  = HELP_CMD  |
                                        SET_CMD   |
                                        LOG_CMD   |
                                        FILE_CMD  |
                                        LIST_CMD  |
                                        MDX_CMD   |
                                        FUNC_CMD  |
                                        PARAM_CMD |
                                        CUBE_CMD  |
                                        ERROR_CMD |
                                        ECHO_CMD  |
                                        EXPR_CMD  |
                                        EXIT_CMD;

    private static final char ESCAPE_CHAR         = '\\';
    private static final char EXECUTE_CHAR        = '=';
    private static final char CANCEL_CHAR         = '~';
    private static final char STRING_CHAR_1       = '"';
    private static final char STRING_CHAR_2       = '\'';
    private static final char BRACKET_START       = '[';
    private static final char BRACKET_END         = ']';

    private static final String SEMI_COLON_STRING = ";";

    //////////////////////////////////////////////////////////////////////////
    // help
    //////////////////////////////////////////////////////////////////////////
    protected static String executeHelp(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        int cmd = UNKNOWN_CMD;

        if (tokens.length == 1) {
            buf.append("Commands:");
            cmd = ALL_CMD;

        } else if (tokens.length == 2) {
            String cmdName = tokens[1];

            if (cmdName.equals("help")) {
                cmd = HELP_CMD;
            } else if (cmdName.equals("set")) {
                cmd = SET_CMD;
            } else if (cmdName.equals("log")) {
                cmd = LOG_CMD;
            } else if (cmdName.equals("file")) {
                cmd = FILE_CMD;
            } else if (cmdName.equals("list")) {
                cmd = LIST_CMD;
            } else if (cmdName.equals("func")) {
                cmd = FUNC_CMD;
            } else if (cmdName.equals("param")) {
                cmd = PARAM_CMD;
            } else if (cmdName.equals("cube")) {
                cmd = CUBE_CMD;
            } else if (cmdName.equals("error")) {
                cmd = ERROR_CMD;
            } else if (cmdName.equals("echo")) {
                cmd = ECHO_CMD;
            } else if (cmdName.equals("exit")) {
                cmd = EXIT_CMD;
            } else {
                cmd = UNKNOWN_CMD;
            }
        }

        if (cmd == UNKNOWN_CMD) {
            buf.append("Unknown help command: ");
            buf.append(mdxCmd);
            buf.append(nl);
            buf.append("Type \"help\" for list of commands");
        }

        if ((cmd & HELP_CMD) != 0) {
            // help
            buf.append(nl);
            appendIndent(buf, 1);
            buf.append("help");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("Prints this text");
        }

        if ((cmd & SET_CMD) != 0) {
            // set
            buf.append(nl);
            appendSet(buf);
        }

        if ((cmd & LOG_CMD) != 0) {
            // set
            buf.append(nl);
            appendLog(buf);
        }

        if ((cmd & FILE_CMD) != 0) {
            // file
            buf.append(nl);
            appendFile(buf);
        }
        if ((cmd & LIST_CMD) != 0) {
            // list
            buf.append(nl);
            appendList(buf);
        }

        if ((cmd & MDX_CMD) != 0) {
            buf.append(nl);
            appendIndent(buf, 1);
            buf.append("<mdx query> <cr> ( '");
            buf.append(EXECUTE_CHAR);
            buf.append("' | '");
            buf.append(CANCEL_CHAR);
            buf.append("' ) <cr>");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("Execute or cancel mdx query.");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("An mdx query may span one or more lines.");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("After the last line of the query has been entered,");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("on the next line a single execute character, '");
            buf.append(EXECUTE_CHAR);
            buf.append("', may be entered");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("followed by a carriage return.");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("The lone '");
            buf.append(EXECUTE_CHAR);
            buf.append("' informs the interpreter that the query has");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("has been entered and is ready to execute.");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("At anytime during the entry of a query the cancel");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("character, '");
            buf.append(CANCEL_CHAR);
            buf.append("', may be entered alone on a line.");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("This removes all of the query text from the");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("the command interpreter.");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("Queries can also be ended by using a semicolon ';'");
            buf.append(nl);
            appendIndent(buf, 3);
            buf.append("at the end of a line.");
        }
        if ((cmd & FUNC_CMD) != 0) {
            buf.append(nl);
            appendFunc(buf);
        }

        if ((cmd & PARAM_CMD) != 0) {
            buf.append(nl);
            appendParam(buf);
        }

        if ((cmd & CUBE_CMD) != 0) {
            buf.append(nl);
            appendCube(buf);
        }

        if ((cmd & ERROR_CMD) != 0) {
            buf.append(nl);
            appendError(buf);
        }

        if ((cmd & ECHO_CMD) != 0) {
            buf.append(nl);
            appendEcho(buf);
        }

        if ((cmd & EXPR_CMD) != 0) {
            buf.append(nl);
            appendExpr(buf);
        }

        if (cmd == ALL_CMD) {
            // reexecute
            buf.append(nl);
            appendIndent(buf, 1);
            buf.append("= <cr>");
            buf.append(nl);
            appendIndent(buf, 2);
            buf.append("Re-Execute mdx query.");
        }

        if ((cmd & EXIT_CMD) != 0) {
            // exit
            buf.append(nl);
            appendExit(buf);
        }


        return buf.toString();
    }

    protected static void appendIndent(StringBuilder buf, int i) {
        while (i-- > 0) {
            buf.append(CmdRunner.INDENT);
        }
    }

    //////////////////////////////////////////////////////////////////////////
    // set
    //////////////////////////////////////////////////////////////////////////
    protected static void appendSet(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("set [ property[=value ] ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With no args, prints all mondrian properties and values.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"property\" prints property's value.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"property=value\" set property to that value.");
    }

    protected String executeSet(String mdxCmd) {
        StringBuilder buf = new StringBuilder(400);

        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            // list all properties
            listPropertiesAll(buf);

        } else if (tokens.length == 2) {
            String arg = tokens[1];
            int index = arg.indexOf('=');
            if (index == -1) {
                listProperty(arg, buf);
            } else {
                String[] nv = arg.split("=");
                String name = nv[0];
                String value = nv[1];
                if (isProperty(name)) {
                    try {
                        if (setProperty(name, value)) {
                            this.connectString = null;
                        }
                    } catch (Exception ex) {
                        setError(ex);
                    }
                } else {
                    buf.append("Bad property name:");
                    buf.append(name);
                    buf.append(nl);
                }
            }

        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendSet(buf);
        }

        return buf.toString();
    }

    //////////////////////////////////////////////////////////////////////////
    // log
    //////////////////////////////////////////////////////////////////////////
    protected static void appendLog(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("log [ classname[=level ] ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With no args, prints the current log level of all classes.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"classname\" prints the current log level of the class.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"classname=level\" set log level to new value.");
    }

    protected String executeLog(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            Enumeration e = LogManager.getCurrentLoggers();
            while (e.hasMoreElements()) {
                Logger logger = (Logger) e.nextElement();
                buf.append(logger.getName());
                buf.append(':');
                buf.append(logger.getLevel());
                buf.append(nl);
            }

        } else if (tokens.length == 2) {
            String arg = tokens[1];
            int index = arg.indexOf('=');
            if (index == -1) {
                Logger logger = LogManager.exists(arg);
                if (logger == null) {
                    buf.append("Bad log name: ");
                    buf.append(arg);
                    buf.append(nl);
                } else {
                    buf.append(logger.getName());
                    buf.append(':');
                    buf.append(logger.getLevel());
                    buf.append(nl);
                }
            } else {
                String[] nv = arg.split("=");
                String classname = nv[0];
                String levelStr = nv[1];

                Logger logger = LogManager.getLogger(classname);

                if (logger == null) {
                    buf.append("Bad log name: ");
                    buf.append(classname);
                    buf.append(nl);
                } else {
                    Level level = Level.toLevel(levelStr, null);
                    if (level == null) {
                        buf.append("Bad log level: ");
                        buf.append(levelStr);
                        buf.append(nl);
                    } else {
                        logger.setLevel(level);
                    }
                }
            }

        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendSet(buf);
        }

        return buf.toString();
    }

    //////////////////////////////////////////////////////////////////////////
    // file
    //////////////////////////////////////////////////////////////////////////
    protected static void appendFile(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("file [ filename | '=' ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With no args, prints the last filename executed.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"filename\", read and execute filename .");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"=\" character, re-read and re-execute previous filename .");
    }

    protected String executeFile(String mdxCmd) {
        StringBuilder buf = new StringBuilder(512);
        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            if (this.filename != null) {
                buf.append(this.filename);
            }

        } else if (tokens.length == 2) {
            String token = tokens[1];
            String nameOfFile = null;
            if ((token.length() == 1) && (token.charAt(0) == EXECUTE_CHAR)) {
                // file '='
                if (this.filename == null) {
                    buf.append("Bad command usage: \"");
                    buf.append(mdxCmd);
                    buf.append("\", no file to re-execute");
                    buf.append(nl);
                    appendFile(buf);
                } else {
                    nameOfFile = this.filename;
                }
            } else {
                // file filename
                nameOfFile = token;
            }

            if (nameOfFile != null) {
                this.filename = nameOfFile;

                try {
                    commandLoop(new File(this.filename));
                } catch (IOException ex) {
                    setError(ex);
                    buf.append("Error: ").append(ex);
                }
            }

        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendFile(buf);
        }
        return buf.toString();
    }

    //////////////////////////////////////////////////////////////////////////
    // list
    //////////////////////////////////////////////////////////////////////////
    protected static void appendList(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("list [ cmd | result ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With no arguments, list previous cmd and result");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"cmd\" argument, list the last mdx query cmd.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"result\" argument, list the last mdx query result.");
    }

    protected String executeList(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            if (this.mdxCmd != null) {
                buf.append(this.mdxCmd);
                if (mdxResult != null) {
                    buf.append(nl);
                    buf.append(mdxResult);
                }
            } else if (mdxResult != null) {
                buf.append(mdxResult);
            }

        } else if (tokens.length == 2) {
            String arg = tokens[1];
            if (arg.equals("cmd")) {
                if (this.mdxCmd != null) {
                    buf.append(this.mdxCmd);
                }
            } else if (arg.equals("result")) {
                if (mdxResult != null) {
                    buf.append(mdxResult);
                }
            } else {
                buf.append("Bad sub command usage:");
                buf.append(mdxCmd);
                buf.append(nl);
                appendList(buf);
            }
        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendList(buf);
        }

        return buf.toString();
    }

    //////////////////////////////////////////////////////////////////////////
    // func
    //////////////////////////////////////////////////////////////////////////
    protected static void appendFunc(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("func [ name ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With no arguments, list all defined function names");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"name\" argument, display the functions:");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append("name, description, and syntax");
    }
    protected String executeFunc(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        final FunTable funTable = getConnection().getSchema().getFunTable();
        if (tokens.length == 1) {
            // prints names only once
            List<FunInfo> funInfoList = funTable.getFunInfoList();
            Iterator<FunInfo> it = funInfoList.iterator();
            String prevName = null;
            while (it.hasNext()) {
                FunInfo fi = it.next();
                String name = fi.getName();
                if (prevName == null || ! prevName.equals(name)) {
                    buf.append(name);
                    buf.append(nl);
                    prevName = name;
                }
            }

        } else if (tokens.length == 2) {
            String funcname = tokens[1];
            List<FunInfo> funInfoList = funTable.getFunInfoList();
            List<FunInfo> matches = new ArrayList<FunInfo>();

            for (FunInfo fi : funInfoList) {
                if (fi.getName().equalsIgnoreCase(funcname)) {
                    matches.add(fi);
                }
            }

            if (matches.size() == 0) {
                buf.append("Bad function name \"");
                buf.append(funcname);
                buf.append("\", usage:");
                buf.append(nl);
                appendList(buf);
            } else {
                Iterator<FunInfo> it = matches.iterator();
                boolean doname = true;
                while (it.hasNext()) {
                    FunInfo fi = it.next();
                    if (doname) {
                        buf.append(fi.getName());
                        buf.append(nl);
                        doname = false;
                    }

                    appendIndent(buf, 1);
                    buf.append(fi.getDescription());
                    buf.append(nl);

                    String[] sigs = fi.getSignatures();
                    if (sigs == null) {
                        appendIndent(buf, 2);
                        buf.append("Signature: ");
                        buf.append("NONE");
                        buf.append(nl);
                    } else {
                        for (String sig : sigs) {
                            appendIndent(buf, 2);
                            buf.append(sig);
                            buf.append(nl);
                        }
                    }
/*
                    appendIndent(buf, 1);
                    buf.append("Return Type: ");
                    int returnType = fi.getReturnTypes();
                    if (returnType >= 0) {
                        buf.append(cat.getName(returnType));
                    } else {
                        buf.append("NONE");
                    }
                    buf.append(nl);
                    int[][] paramsArray = fi.getParameterTypes();
                    if (paramsArray == null) {
                        appendIndent(buf, 1);
                        buf.append("Paramter Types: ");
                        buf.append("NONE");
                        buf.append(nl);

                    } else {
                        for (int j = 0; j < paramsArray.length; j++) {
                            int[] params = paramsArray[j];
                            appendIndent(buf, 1);
                            buf.append("Paramter Types: ");
                            for (int k = 0; k < params.length; k++) {
                                int param = params[k];
                                buf.append(cat.getName(param));
                                buf.append(' ');
                            }
                            buf.append(nl);
                        }
                    }
*/
                }
            }
        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendList(buf);
        }

        return buf.toString();
    }
    //////////////////////////////////////////////////////////////////////////
    // param
    //////////////////////////////////////////////////////////////////////////
    protected static void appendParam(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("param [ name[=value ] ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With no argumnts, all param name/value pairs are printed.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"name\" argument, the value of the param is printed.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"name=value\" sets the parameter with name to value.");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append(" If name is null, then unsets all parameters");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append(
            " If value is null, then unsets the parameter associated with "
            + "value");
    }
    protected String executeParam(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            // list all properties
            listParameterNameValues(buf);

        } else if (tokens.length == 2) {
            String arg = tokens[1];
            int index = arg.indexOf('=');
            if (index == -1) {
                if (isParam(arg)) {
                    listParam(arg, buf);
                } else {
                    buf.append("Bad parameter name:");
                    buf.append(arg);
                    buf.append(nl);
                }
            } else {
                String[] nv = arg.split("=");
                String name = (nv.length == 0) ? null : nv[0];
                String value = (nv.length == 2) ? nv[1] : null;
                setParameter(name, value);
            }

        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendSet(buf);
        }

        return buf.toString();
    }
    //////////////////////////////////////////////////////////////////////////
    // cube
    //////////////////////////////////////////////////////////////////////////
    protected static void appendCube(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("cube [ cubename [ name [=value | command] ] ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With no argumnts, all cubes are listed by name.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"cubename\" argument, cube attribute name/values for:");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append("fact table (readonly)");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append("aggregate caching (readwrite)");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("are printed");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"cubename name=value\" sets the readwrite attribute with "
            + "name to value.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"cubename command\" executes the commands:");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append("clearCache");
    }

    protected String executeCube(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            // list all properties
            listCubeName(buf);
        } else if (tokens.length == 2) {
            String cubename = tokens[1];
            listCubeAttribues(cubename, buf);

        } else if (tokens.length == 3) {
            String cubename = tokens[1];
            String arg = tokens[2];
            int index = arg.indexOf('=');
            if (index == -1) {
                // its a commnd
                executeCubeCommand(cubename, arg, buf);
            } else {
                String[] nv = arg.split("=");
                String name = (nv.length == 0) ? null : nv[0];
                String value = (nv.length == 2) ? nv[1] : null;
                setCubeAttribute(cubename, name, value, buf);
            }

        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendSet(buf);
        }

        return buf.toString();
    }
    //////////////////////////////////////////////////////////////////////////
    // error
    //////////////////////////////////////////////////////////////////////////
    protected static void appendError(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("error [ msg | stack ] <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With no argumnts, both message and stack are printed.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("With \"msg\" argument, the Error message is printed.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append(
            "With \"stack\" argument, the Error stack trace is printed.");
    }

    protected String executeError(String mdxCmd) {
        StringBuilder buf = new StringBuilder(200);

        String[] tokens = mdxCmd.split("\\s+");

        if (tokens.length == 1) {
            if (error != null) {
                buf.append(error);
                if (stack != null) {
                    buf.append(nl);
                    buf.append(stack);
                }
            } else if (stack != null) {
                buf.append(stack);
            }

        } else if (tokens.length == 2) {
            String arg = tokens[1];
            if (arg.equals("msg")) {
                if (error != null) {
                    buf.append(error);
                }
            } else if (arg.equals("stack")) {
                if (stack != null) {
                    buf.append(stack);
                }
            } else {
                buf.append("Bad sub command usage:");
                buf.append(mdxCmd);
                buf.append(nl);
                appendList(buf);
            }
        } else {
            buf.append("Bad command usage: \"");
            buf.append(mdxCmd);
            buf.append('"');
            buf.append(nl);
            appendList(buf);
        }

        return buf.toString();
    }

    //////////////////////////////////////////////////////////////////////////
    // echo
    //////////////////////////////////////////////////////////////////////////
    protected static void appendEcho(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("echo text <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("echo text to standard out.");
    }

    protected String executeEcho(String mdxCmd) {
        try {
            String resultString = (mdxCmd.length() == 4)
                ? "" : mdxCmd.substring(4);
            return resultString;
        } catch (Exception ex) {
            setError(ex);
            //return error;
            return null;
        }
    }
    //////////////////////////////////////////////////////////////////////////
    // expr
    //////////////////////////////////////////////////////////////////////////
    protected static void appendExpr(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("expr cubename expression<cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("evaluate an expression against a cube.");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("where: ");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append("cubename is single word or string using [], '' or \"\"");
        buf.append(nl);
        appendIndent(buf, 3);
        buf.append("expression is string using \"\"");
    }
    protected String executeExpr(String mdxCmd) {
        StringBuilder buf = new StringBuilder(256);

        mdxCmd = (mdxCmd.length() == 5)
                ? "" : mdxCmd.substring(5);

        String regex = "(\"[^\"]+\"|'[^\']+'|\\[[^\\]]+\\]|[^\\s]+)\\s+.*";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(mdxCmd);
        boolean b = m.matches();

        if (! b) {
            buf.append("Could not parse into \"cubename expression\" command:");
            buf.append(nl);
            buf.append(mdxCmd);
            String msg = buf.toString();
            setError(msg);
            return msg;
        } else {
            String cubeName = m.group(1);
            String expression = mdxCmd.substring(cubeName.length() + 1);

            if (cubeName.charAt(0) == '"') {
                cubeName = cubeName.substring(1, cubeName.length() - 1);
            } else if (cubeName.charAt(0) == '\'') {
                cubeName = cubeName.substring(1, cubeName.length() - 1);
            } else if (cubeName.charAt(0) == '[') {
                cubeName = cubeName.substring(1, cubeName.length() - 1);
            }

            int len = expression.length();
            if (expression.charAt(0) == '"') {
                if (expression.charAt(len - 1) != '"') {
                    buf.append("Missing end '\"' in expression:");
                    buf.append(nl);
                    buf.append(expression);
                    String msg = buf.toString();
                    setError(msg);
                    return msg;
                }
                expression = expression.substring(1, len - 1);

            } else if (expression.charAt(0) == '\'') {
                if (expression.charAt(len - 1) != '\'') {
                    buf.append("Missing end \"'\" in expression:");
                    buf.append(nl);
                    buf.append(expression);
                    String msg = buf.toString();
                    setError(msg);
                    return msg;
                }
                expression = expression.substring(1, len - 1);
            }

            Cube cube = getCube(cubeName);
            if (cube == null) {
                buf.append("No cube found with name \"");
                buf.append(cubeName);
                buf.append("\"");
                String msg = buf.toString();
                setError(msg);
                return msg;

            } else {
                try {
                    if (cubeName.indexOf(' ') >= 0) {
                        if (cubeName.charAt(0) != '[') {
                            cubeName = Util.quoteMdxIdentifier(cubeName);
                        }
                    }
                    final char c = '\'';
                    if (expression.indexOf('\'') != -1) {
                        // make sure all "'" are escaped
                        int start = 0;
                        int index = expression.indexOf('\'', start);
                        if (index == 0) {
                            // error: starts with "'"
                            buf.append("Double \"''\" starting expression:");
                            buf.append(nl);
                            buf.append(expression);
                            String msg = buf.toString();
                            setError(msg);
                            return msg;
                        }
                        while (index != -1) {
                            if (expression.charAt(index - 1) != '\\') {
                                // error
                                buf.append("Non-escaped \"'\" in expression:");
                                buf.append(nl);
                                buf.append(expression);
                                String msg = buf.toString();
                                setError(msg);
                                return msg;
                            }
                            start = index + 1;
                            index = expression.indexOf('\'', start);
                        }
                    }

                    // taken from FoodMartTest code
                    StringBuilder queryStringBuf = new StringBuilder(64);
                    queryStringBuf.append("with member [Measures].[Foo] as ");
                    queryStringBuf.append(c);
                    queryStringBuf.append(expression);
                    queryStringBuf.append(c);
                    queryStringBuf.append(
                        " select {[Measures].[Foo]} on columns from ");
                    queryStringBuf.append(cubeName);

                    String queryString = queryStringBuf.toString();

                    Result result = runQuery(queryString, true);
                    String resultString =
                        result.getCell(new int[]{0}).getFormattedValue();
                    mdxResult = resultString;
                    clearError();

                    buf.append(resultString);
                } catch (Exception ex) {
                    setError(ex);
                    buf.append("Error: ").append(ex);
                }
            }
        }
        return buf.toString();
    }
    //////////////////////////////////////////////////////////////////////////
    // exit
    //////////////////////////////////////////////////////////////////////////
    protected static void appendExit(StringBuilder buf) {
        appendIndent(buf, 1);
        buf.append("exit <cr>");
        buf.append(nl);
        appendIndent(buf, 2);
        buf.append("Exit mdx command interpreter.");
    }


    protected String reExecuteMdxCmd() {
        if (this.mdxCmd == null) {
            return "No command to execute";
        } else {
            return executeMdxCmd(this.mdxCmd);
        }
    }

    protected String executeMdxCmd(String mdxCmd) {
        this.mdxCmd = mdxCmd;
        try {
            String resultString = execute(mdxCmd);
            mdxResult = resultString;
            clearError();
            return resultString;
        } catch (Exception ex) {
            setError(ex);
            //return error;
            return null;
        }
    }

    /////////////////////////////////////////////////////////////////////////
    // helpers
    /////////////////////////////////////////////////////////////////////////
    protected static void loadPropertiesFromFile(
        String propFile)
        throws IOException
    {
        MondrianProperties.instance().load(new FileInputStream(propFile));
    }

    /////////////////////////////////////////////////////////////////////////
    // main
    /////////////////////////////////////////////////////////////////////////

    /**
     * Prints a usage message.
     *
     * @param msg Prefix to the message
     * @param out Output stream
     */
    protected static void usage(String msg, PrintStream out) {
        StringBuilder buf = new StringBuilder(256);
        if (msg != null) {
            buf.append(msg);
            buf.append(nl);
        }
        buf.append(
            "Usage: mondrian.tui.CmdRunner args"
            + nl
            + "  args:"
            + nl
            + "  -h               : print this usage text"
            + nl
            + "  -H               : ready to print out high cardinality"
            + nl
            + "                     dimensions"
            + nl
            + "  -d               : enable local debugging"
            + nl
            + "  -t               : time each mdx query"
            + nl
            + "  -nocache         : turn off in-memory aggregate caching"
            + nl
            + "                     for all cubes regardless of setting"
            + nl
            + "                     in schema"
            + nl
            + "  -rc              : do NOT reload connections each query"
            + nl
            + "                     (default is to reload connections)"
            + nl
            + "  -p propertyfile  : load mondrian properties"
            + nl
            + "  -r role_name     : set the connections role name"
            + nl
            + "  -f mdx_filename+ : execute mdx in one or more files"
            + nl
            + "  -x xmla_filename+: execute XMLA in one or more files"
            + "                     the XMLA request has no SOAP wrapper"
            + nl
            + "  -xs soap_xmla_filename+ "
            + "                   : execute Soap XMLA in one or more files"
            + "                     the XMLA request has a SOAP wrapper"
            + nl
            + "  -vt              : validate xmla response using transforms"
            + "                     only used with -x or -xs flags"
            + nl
            + "  -vx              : validate xmla response using xpaths"
            + "                     only used with -x or -xs flags"
            + nl
            + "  mdx_cmd          : execute mdx_cmd"
            + nl);

        out.println(buf.toString());
    }

    /**
     * Set the default comment delimiters for CmdRunner.  These defaults are
     * # to end of line
     * plus all the comment delimiters in Scanner.
     */
    private static void setDefaultCommentState() {
        allowNestedComments = mondrian.olap.Scanner.getNestedCommentsState();
        String[][] scannerCommentsDelimiters =
            mondrian.olap.Scanner.getCommentDelimiters();
        commentDelim = new String[scannerCommentsDelimiters.length + 1][2];
        commentStartChars = new char[scannerCommentsDelimiters.length + 1];


        // CmdRunner has extra delimiter; # to end of line
        commentDelim[0][0] = "#";
        commentDelim[0][1] = null;
        commentStartChars[0] = commentDelim[0][0].charAt(0);


        // copy all the rest of the delimiters
        for (int x = 0; x < scannerCommentsDelimiters.length; x++) {
            commentDelim[x + 1][0] = scannerCommentsDelimiters[x][0];
            commentDelim[x + 1][1] = scannerCommentsDelimiters[x][1];
            commentStartChars[x + 1] = commentDelim[x + 1][0].charAt(0);
        }
    }

    private static final int DO_MDX             = 1;
    private static final int DO_XMLA            = 2;
    private static final int DO_SOAP_XMLA       = 3;

    private static final int VALIDATE_NONE      = 1;
    private static final int VALIDATE_TRANSFORM = 2;
    private static final int VALIDATE_XPATH     = 3;

    protected static class Options {
        private boolean debug = false;
        private boolean timeQueries;
        private boolean noCache = false;
        private String roleName;
        private int validateXmlaResponse = VALIDATE_NONE;
        private final List<String> filenames = new ArrayList<String>();
        private int doingWhat = DO_MDX;
        private String singleMdxCmd;
        private boolean highCardResults;
    }

    public static void main(String[] args) throws Exception {
        Options options;
        try {
            options = parseOptions(args);
        } catch (BadOption badOption) {
            usage(badOption.getMessage(), System.out);
            Throwable t = badOption.getCause();
            if (t != null) {
                System.out.println(t);
                t.printStackTrace();
            }
            return;
        }

        CmdRunner cmdRunner =
                new CmdRunner(options, new PrintWriter(System.out));
        if (options.noCache) {
            cmdRunner.noCubeCaching();
        }

        if (!options.filenames.isEmpty()) {
            for (String filename : options.filenames) {
                cmdRunner.filename = filename;
                switch (options.doingWhat) {
                case DO_MDX:
                    // its a file containing mdx
                    cmdRunner.commandLoop(new File(filename));
                    break;
                case DO_XMLA:
                    // its a file containing XMLA
                    cmdRunner.processXmla(
                        new File(filename),
                        options.validateXmlaResponse);
                    break;
                default:
                    // its a file containing SOAP XMLA
                    cmdRunner.processSoapXmla(
                        new File(filename),
                        options.validateXmlaResponse);
                    break;
                }
                if (cmdRunner.error != null) {
                    System.err.println(filename);
                    System.err.println(cmdRunner.error);
                    if (cmdRunner.stack != null) {
                        System.err.println(cmdRunner.stack);
                    }
                    cmdRunner.printQueryTime();
                    cmdRunner.clearError();
                }
            }
        } else if (options.singleMdxCmd != null) {
            cmdRunner.commandLoop(options.singleMdxCmd, false);
            if (cmdRunner.error != null) {
                System.err.println(cmdRunner.error);
                if (cmdRunner.stack != null) {
                    System.err.println(cmdRunner.stack);
                }
            }
        } else {
            cmdRunner.commandLoop(true);
        }
        cmdRunner.printTotalQueryTime();
    }

    private void printTotalQueryTime() {
        if (options.timeQueries) {
            // only print if different
            if (totalQueryTime != queryTime) {
                out.println("total[" + totalQueryTime + "ms]");
            }
        }
        out.flush();
    }

    private static Options parseOptions(String[] args)
        throws BadOption, IOException
    {
        final Options options = new Options();
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];

            if (arg.equals("-h")) {
                throw new BadOption(null);
            } else if (arg.equals("-H")) {
                options.highCardResults = true;

            } else if (arg.equals("-d")) {
                options.debug = true;

            } else if (arg.equals("-t")) {
                options.timeQueries = true;

            } else if (arg.equals("-nocache")) {
                options.noCache = true;

            } else if (arg.equals("-rc")) {
                CmdRunner.RELOAD_CONNECTION = false;

            } else if (arg.equals("-vt")) {
                options.validateXmlaResponse = VALIDATE_TRANSFORM;

            } else if (arg.equals("-vx")) {
                options.validateXmlaResponse = VALIDATE_XPATH;

            } else if (arg.equals("-f")) {
                i++;
                if (i == args.length) {
                    throw new BadOption("no mdx filename given");
                }
                options.filenames.add(args[i]);

            } else if (arg.equals("-x")) {
                i++;
                if (i == args.length) {
                    throw new BadOption("no XMLA filename given");
                }
                options.doingWhat = DO_XMLA;
                options.filenames.add(args[i]);

            } else if (arg.equals("-xs")) {
                i++;
                if (i == args.length) {
                    throw new BadOption("no XMLA filename given");
                }
                options.doingWhat = DO_SOAP_XMLA;
                options.filenames.add(args[i]);

            } else if (arg.equals("-p")) {
                i++;
                if (i == args.length) {
                    throw new BadOption("no mondrian properties file given");
                }
                String propFile = args[i];
                loadPropertiesFromFile(propFile);

            } else if (arg.equals("-r")) {
                i++;
                if (i == args.length) {
                    throw new BadOption("no role name given");
                }
                options.roleName = args[i];
            } else if (!options.filenames.isEmpty()) {
                options.filenames.add(arg);
            } else {
                options.singleMdxCmd = arg;
            }
        }
        return options;
    }

    private static class BadOption extends Exception {
        BadOption(String msg) {
            super(msg);
        }
        BadOption(String msg, Exception ex) {
            super(msg, ex);
        }
    }
}

// End CmdRunner.java
