All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.specrunner.sql.database.impl.DatabaseDefault Maven / Gradle / Ivy

/*
    SpecRunner - Acceptance Test Driven Development Tool
    Copyright (C) 2011-2016  Thiago Santos

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see 
 */
package org.specrunner.sql.database.impl;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.specrunner.SRServices;
import org.specrunner.comparators.ComparatorException;
import org.specrunner.comparators.IComparator;
import org.specrunner.comparators.core.ComparatorDate;
import org.specrunner.context.IContext;
import org.specrunner.converters.ConverterException;
import org.specrunner.converters.IConverter;
import org.specrunner.converters.IConverterReverse;
import org.specrunner.expressions.EMode;
import org.specrunner.expressions.INullEmptyHandler;
import org.specrunner.expressions.core.NullEmptyHandlerDefault;
import org.specrunner.features.IFeatureManager;
import org.specrunner.formatters.FormatterException;
import org.specrunner.formatters.IFormatter;
import org.specrunner.parameters.DontEval;
import org.specrunner.plugins.PluginException;
import org.specrunner.readers.IReader;
import org.specrunner.readers.ReaderException;
import org.specrunner.result.IResultSet;
import org.specrunner.result.status.Failure;
import org.specrunner.result.status.Success;
import org.specrunner.sql.PluginFilter;
import org.specrunner.sql.database.CommandType;
import org.specrunner.sql.database.DatabaseException;
import org.specrunner.sql.database.DatabaseRegisterEvent;
import org.specrunner.sql.database.DatabaseTableEvent;
import org.specrunner.sql.database.IColumnReader;
import org.specrunner.sql.database.IDatabase;
import org.specrunner.sql.database.IDatabaseListener;
import org.specrunner.sql.database.IIdManager;
import org.specrunner.sql.database.ISequenceProvider;
import org.specrunner.sql.database.ISqlWrapperFactory;
import org.specrunner.sql.database.IStatementFactory;
import org.specrunner.sql.database.SqlWrapper;
import org.specrunner.sql.meta.Column;
import org.specrunner.sql.meta.IDataFilter;
import org.specrunner.sql.meta.IRegister;
import org.specrunner.sql.meta.ReplicableException;
import org.specrunner.sql.meta.Schema;
import org.specrunner.sql.meta.Table;
import org.specrunner.sql.meta.UtilNames;
import org.specrunner.sql.meta.Value;
import org.specrunner.sql.meta.impl.DataFilterDefault;
import org.specrunner.sql.meta.impl.UtilSchema;
import org.specrunner.util.UtilLog;
import org.specrunner.util.UtilSql;
import org.specrunner.util.aligner.core.DefaultAlignmentException;
import org.specrunner.util.cache.ICache;
import org.specrunner.util.cache.ICacheFactory;
import org.specrunner.util.collections.ReverseIterable;
import org.specrunner.util.expression.UtilExpression;
import org.specrunner.util.xom.IPresentation;
import org.specrunner.util.xom.UtilNode;
import org.specrunner.util.xom.core.PresentationCompare;
import org.specrunner.util.xom.core.PresentationException;
import org.specrunner.util.xom.node.CellAdapter;
import org.specrunner.util.xom.node.INodeHolder;
import org.specrunner.util.xom.node.RowAdapter;
import org.specrunner.util.xom.node.TableAdapter;

import nu.xom.Attribute;
import nu.xom.Element;

/**
 * Basic implementation of IDatabase using cached prepared
 * statements, an ID manager to work with generated keys, a sequence provider to
 * enable sequence interactions and a column reader to recover column objects.
 * 
 * @author Thiago Santos
 * 
 */
@SuppressWarnings("serial")
public class DatabaseDefault implements IDatabase {

    /**
     * Feature for comparison filter.
     */
    public static final String FEATURE_FILTER = DatabaseDefault.class.getName() + ".filter";
    /**
     * Pattern name to be used.
     */
    private String filter;

    /**
     * A null/empty handler.
     */
    protected INullEmptyHandler nullEmptyHandler = new NullEmptyHandlerDefault();

    /**
     * Sequence next value generator.
     */
    protected ISequenceProvider sequenceProvider = new SequenceProviderDefault();

    /**
     * Recover object from a result set column to be compared against the
     * specification object.
     */
    protected IColumnReader columnReader = new ColumnReaderDefault();

    /**
     * Factory of SQLs.
     */
    protected ISqlWrapperFactory sqlWrapperFactory = new SqlWrapperFactoryDefault();

    /**
     * Factory of statements.
     */
    protected IStatementFactory statementFactory = new StatementFactoryDefault();

    /**
     * Manage object lookup and reuse.
     */
    protected IIdManager idManager = new IdManagerDefault();

    /**
     * List of listeners.
     */
    protected List listeners = new LinkedList();
    /**
     * Reuse script status.
     */
    protected Boolean reuseScripts = Boolean.FALSE;
    /**
     * Database name.
     */
    protected String name;
    /**
     * Cache of tables tables to scripts.
     */
    protected static ICache xmlToSql = SRServices.get(ICacheFactory.class).newCache(DatabaseDefault.class.getName());
    /**
     * Feature to use MD5 keys on cache.
     */
    public static final String FEATURE_MD5_KEYS = DatabaseDefault.class.getName() + ".md5Keys";
    /**
     * Use tables MD5 as keys.
     */
    protected Boolean md5Keys = Boolean.FALSE;
    /**
     * Message digester.
     */
    protected MessageDigest digester;

    /**
     * Feature for database error dump limit.
     */
    public static final String FEATURE_LIMIT = DatabaseDefault.class.getName() + ".limit";

    /**
     * Feature for dump size.
     */
    private static final Integer DEFAULT_LIMIT = 100;

    /**
     * Max size of errors dump.
     */
    private Integer limit = DEFAULT_LIMIT;

    @Override
    public void initialize() {
        IFeatureManager fm = SRServices.getFeatureManager();
        fm.set(FEATURE_FILTER, this);
        fm.set(FEATURE_NULL_EMPTY_HANDLER, this);
        fm.set(FEATURE_SEQUENCE_PROVIDER, this);
        fm.set(FEATURE_COLUMN_READER, this);
        fm.set(FEATURE_SQL_WRAPPER_FACTORY, this);
        fm.set(FEATURE_STATEMENT_FACTORY, this);
        fm.set(FEATURE_ID_MANAGER, this);
        fm.set(FEATURE_LISTENERS, this);
        fm.set(FEATURE_REUSE_SCRIPTS, this);
        fm.set(FEATURE_MD5_KEYS, this);
        fm.set(FEATURE_LIMIT, this);
        // every use of database clear mappings to avoid memory overload and
        // test interference
        idManager.reset();
    }

    /**
     * Get the filter name.
     * 
     * @return The name.
     */
    public String getFilter() {
        return filter;
    }

    /**
     * Set the filter name.
     * 
     * @param filter
     *            Name.
     */
    @DontEval
    public void setFilter(String filter) {
        this.filter = filter;
    }

    /**
     * Get the null/empty handler.
     * 
     * @return Current null/empty handler.
     */
    public INullEmptyHandler getNullEmptyHandler() {
        return nullEmptyHandler;
    }

    @Override
    public void setNullEmptyHandler(INullEmptyHandler nullEmptyHandler) {
        this.nullEmptyHandler = nullEmptyHandler;
    }

    /**
     * Get the sequence values provider.
     * 
     * @return The provider.
     */
    public ISequenceProvider getSequenceProvider() {
        return sequenceProvider;
    }

    @Override
    public void setSequenceProvider(ISequenceProvider sequenceProvider) {
        this.sequenceProvider = sequenceProvider;
    }

    /**
     * Get current column reader.
     * 
     * @return The current reader.
     */
    public IColumnReader getColumnReader() {
        return columnReader;
    }

    @Override
    public void setColumnReader(IColumnReader columnReader) {
        this.columnReader = columnReader;
    }

    /**
     * Get the SQL wrapper factory.
     * 
     * @return The current factory.
     */
    public ISqlWrapperFactory getSqlWrapperFactory() {
        return sqlWrapperFactory;
    }

    @Override
    public void setSqlWrapperFactory(ISqlWrapperFactory sqlWrapperFactory) {
        this.sqlWrapperFactory = sqlWrapperFactory;
    }

    /**
     * Get statement factory.
     * 
     * @return The current factory.
     */
    public IStatementFactory getStatementFactory() {
        return statementFactory;
    }

    @Override
    public void setStatementFactory(IStatementFactory statementFactory) {
        this.statementFactory = statementFactory;
    }

    /**
     * Get the id manager.
     * 
     * @return The manager.
     */
    public IIdManager getIdManager() {
        return idManager;
    }

    @Override
    public void setIdManager(IIdManager idManager) {
        this.idManager = idManager;
    }

    /**
     * Get listeners.
     * 
     * @return Listeners.
     */
    public List getListeners() {
        return listeners;
    }

    @Override
    public void setListeners(List listeners) {
        if (listeners == null) {
            throw new IllegalArgumentException("Listeners cannot be a null list.");
        }
        this.listeners = listeners;
    }

    /**
     * Get reuse status.
     * 
     * @return true, if enabled, false, otherwise.
     */
    public Boolean getReuseScripts() {
        return reuseScripts;
    }

    @Override
    public void setReuseScripts(Boolean reuseScripts) {
        this.reuseScripts = reuseScripts;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Check if MD5 keys are enabled.
     * 
     * @return true, if enabled, false, otherwise. Default is 'false'.
     */
    public Boolean getMd5Keys() {
        return md5Keys;
    }

    /**
     * Set MD5 flag.
     * 
     * @param md5Keys
     *            true, for MD5 keys, false, otherwise.
     */
    public void setMd5Keys(Boolean md5Keys) {
        this.md5Keys = md5Keys;
    }

    /**
     * Get the error dump limit.
     * 
     * @return The limit.
     */
    public Integer getLimit() {
        return limit;
    }

    /**
     * Set error limit.
     * 
     * @param limit
     *            The limit.
     */
    public void setLimit(Integer limit) {
        this.limit = limit;
    }

    /**
     * Fire initialize event.
     */
    protected void fireInitialize() {
        synchronized (listeners) {
            for (IDatabaseListener listener : listeners) {
                listener.initialize();
            }
        }
    }

    /**
     * Fire table in event.
     * 
     * @param event
     *            Event.
     * @throws DatabaseException
     *             On processing errors.
     */
    protected void fireTableIn(DatabaseTableEvent event) throws DatabaseException {
        synchronized (listeners) {
            for (IDatabaseListener listener : listeners) {
                listener.onTableIn(event);
            }
        }
    }

    /**
     * Fire register in event.
     * 
     * @param event
     *            Event.
     * @throws DatabaseException
     *             On processing errors.
     */
    protected void fireRegisterIn(DatabaseRegisterEvent event) throws DatabaseException {
        synchronized (listeners) {
            for (IDatabaseListener listener : listeners) {
                listener.onRegisterIn(event);
            }
        }
    }

    /**
     * Fire register out event.
     * 
     * @param event
     *            Event.
     * @throws DatabaseException
     *             On processing errors.
     */
    protected void fireRegisterOut(DatabaseRegisterEvent event) throws DatabaseException {
        synchronized (listeners) {
            for (IDatabaseListener listener : new ReverseIterable(listeners)) {
                listener.onRegisterOut(event);
            }
        }
    }

    /**
     * Fire table out event.
     * 
     * @param event
     *            Event.
     * @throws DatabaseException
     *             On processing errors.
     */
    protected void fireTableOut(DatabaseTableEvent event) throws DatabaseException {
        synchronized (listeners) {
            for (IDatabaseListener listener : new ReverseIterable(listeners)) {
                listener.onTableOut(event);
            }
        }
    }

    @Override
    public void perform(IContext context, IResultSet result, TableAdapter adapter, Connection connection, Schema schema, EMode mode) throws DatabaseException {
        IDataFilter afilter = getFilter(context, mode, schema);
        if (!afilter.accept(mode, schema)) {
            if (UtilLog.LOG.isInfoEnabled()) {
                UtilLog.LOG.info("Schema ignored[" + mode + "]:" + schema.getAlias() + "(" + schema.getName() + ")");
            }
            UtilNode.appendCss(adapter.getNode(), IDataFilter.CSS_SCHEMA);
            return;
        }
        String tAlias = adapter.getAttribute("caption");
        if (tAlias == null) {
            List captions = adapter.getCaptions();
            if (captions.isEmpty()) {
                throw new DatabaseException("Tables must have a caption. The caption must be part of this set: " + schema.getAliasToTables().keySet());
            }
            tAlias = captions.get(0).getValue(context);
        }
        Table table = schema.getAlias(tAlias);
        if (table == null) {
            throw new DatabaseException("Table '" + tAlias + "' [as '" + UtilNames.normalize(tAlias) + "'] not found in schema " + schema.getAlias() + "(" + schema.getName() + "), available alias: " + schema.getAliasToTables().keySet() + ", available tables: " + schema.getNamesToTables().keySet());
        }
        if (!afilter.accept(mode, table)) {
            if (UtilLog.LOG.isInfoEnabled()) {
                UtilLog.LOG.info("Table ignored[" + mode + "]:" + table.getAlias() + "(" + table.getName() + ")");
            }
            UtilNode.appendCss(adapter.getNode(), IDataFilter.CSS_TABLE);
            return;
        }
        if (reuseScripts && mode == EMode.INPUT) {
            synchronized (xmlToSql) {
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Reuse scripts activated for '" + table.getAlias() + "/" + table.getName() + "'.");
                }
                String xml = adapter.toXML();
                if (md5Keys) {
                    if (digester == null) {
                        try {
                            digester = MessageDigest.getInstance("MD5");
                        } catch (NoSuchAlgorithmException e) {
                            throw new DatabaseException("Could not generate MD5 keys for tables.", e);
                        }
                    }
                    digester.reset();
                    digester.update(xml.getBytes());
                    BigInteger number = new BigInteger(1, digester.digest());
                    xml = String.valueOf(number);
                    if (UtilLog.LOG.isInfoEnabled()) {
                        UtilLog.LOG.info("MD5 generated: " + xml);
                    }
                }
                String sql = xmlToSql.get(xml);
                if (sql == null) {
                    List old = getListeners();
                    try {
                        final StringBuilder tmp = new StringBuilder();
                        List lista = new LinkedList(old);
                        // this listeners captures SQL dump to a buffer
                        lista.add(new DatabasePrintListener() {
                            @Override
                            protected void print(StringBuilder sb) {
                                tmp.append(sb);
                                tmp.append('\n');
                            }
                        });
                        // change the listener before perform data actions
                        setListeners(lista);
                        processTable(context, result, adapter, connection, mode, afilter, table);
                        // after processing data SQL script generated is
                        // available
                        xmlToSql.put(xml, tmp.toString());
                        if (UtilLog.LOG.isInfoEnabled()) {
                            UtilLog.LOG.info("Saved a script for table '" + table.getAlias() + "/" + table.getName() + "'.");
                        }
                        if (UtilLog.LOG.isTraceEnabled()) {
                            UtilLog.LOG.trace("CACHE FOR '" + table.getAlias() + "/" + table.getName() + "':\n" + xml + "\n IS \n" + tmp);
                        }
                    } finally {
                        // return listeners to previous state
                        setListeners(old);
                    }
                } else {
                    if (UtilLog.LOG.isInfoEnabled()) {
                        UtilLog.LOG.info("Reusing script " + (md5Keys ? "(MD5:" + xml + ")" : "") + " for table: '" + table.getAlias() + "/" + table.getName() + "'.");
                    }
                    if (!sql.isEmpty()) {
                        Statement stmt = null;
                        try {
                            stmt = connection.createStatement();
                            stmt.execute(sql);
                            if (UtilLog.LOG.isInfoEnabled()) {
                                UtilLog.LOG.info("Reused '" + table.getAlias() + "/" + table.getName() + "'.");
                            }
                            if (UtilLog.LOG.isTraceEnabled()) {
                                UtilLog.LOG.trace("SCRIPT:\n" + sql + ".");
                            }
                        } catch (SQLException e) {
                            throw new DatabaseException("Script errors: " + e.getMessage(), e);
                        } finally {
                            if (stmt != null) {
                                try {
                                    stmt.close();
                                } catch (SQLException e) {
                                    throw new DatabaseException("Could not close statement: " + e.getMessage(), e);
                                }
                            }
                        }
                    }
                }
            }
        } else {
            processTable(context, result, adapter, connection, mode, afilter, table);
        }
    }

    /**
     * Process a data table.
     * 
     * @param context
     *            A context.
     * @param result
     *            A result.
     * @param adapter
     *            A table adapter.
     * @param connection
     *            A connection.
     * @param mode
     *            A mode.
     * @param afilter
     *            A filter.
     * @param table
     *            A schema table.
     * @throws DatabaseException
     *             On processing errors.
     */
    protected void processTable(IContext context, IResultSet result, TableAdapter adapter, Connection connection, EMode mode, IDataFilter afilter, Table table) throws DatabaseException {
        // creates a copy only of defined tables
        try {
            table = table.copy();
        } catch (ReplicableException e) {
            throw new DatabaseException("Cannot create a copy of table " + table.getName() + " with alias " + table.getAlias() + ".", e);
        }
        List rows = adapter.getRows();
        if (rows.isEmpty()) {
            throw new DatabaseException("A valid table should have at least 1 row for headers (th's).");
        }
        // headers
        RowAdapter header = null;
        int headerIndex = 0;
        for (int i = 0; i < adapter.getRowCount(); i++) {
            headerIndex = i;
            RowAdapter tmp = adapter.getRow(i);
            if (!UtilNode.isIgnore(tmp.getNode())) {
                header = tmp;
                break;
            }
        }
        if (header == null) {
            throw new DatabaseException(".");
        }

        List headers = header.getCells();
        Column[] columns = new Column[headers.size()];
        readHeadersColumns(context, mode, table, headers, columns, afilter);

        // clear listeners
        fireInitialize();

        // start using listeners
        if (!listeners.isEmpty()) {
            fireTableIn(new DatabaseTableEvent(context, result, adapter, this, connection, table, mode));
        }

        String defaultType = adapter.getAttribute("action");
        for (int i = headerIndex + 1; i < rows.size(); i++) {
            RowAdapter row = rows.get(i);
            if(UtilNode.isIgnore(row.getNode())) {
                continue;
            }
            List tds = row.getCells();
            if (tds.isEmpty()) {
                throw new DatabaseException("Empty lines are useless. Invalid row[" + i + "]:" + row.getValue(context));
            }
            if (tds.size() != headers.size()) {
                throw new DatabaseException("Invalid number of cells at row: " + i + ". Expected " + headers.size() + " columns, received " + tds.size() + ".\n\t ROW:" + row);
            }
            int expectedCount = Integer.parseInt(row.getAttribute("count", "1"));
            String type = defaultType != null ? defaultType : tds.get(0).getValue(context);
            CommandType command = CommandType.get(type);
            if (command == null) {
                throw new DatabaseException("Invalid command type. '" + type + "' at (row: " + i + ", cell: 0). The first column is required for one of the following values: " + Arrays.toString(CommandType.values()));
            }
            Map filled = new HashMap();
            Map missing = new HashMap();
            IRegister register = new RegisterDefault(table);
            for (int j = (defaultType != null ? 0 : 1); j < tds.size(); j++) {
                if (columns[j] == null) {
                    // ignored column;
                    continue;
                }
                Column column = columns[j].copy();
                CellAdapter td = tds.get(j);
                try {
                    UtilSchema.setupColumn(context, table, column, td);
                } catch (ReaderException e) {
                    throw new DatabaseException(e);
                } catch (ConverterException e) {
                    throw new DatabaseException(e);
                } catch (FormatterException e) {
                    throw new DatabaseException(e);
                } catch (ComparatorException e) {
                    throw new DatabaseException(e);
                }
                String content = getAdjustContent(context, mode, command, column, afilter, td);
                try {
                    Value v = getValue(context, mode, command, column, afilter, td, content);
                    if (v != null) {
                        register.add(v);
                        filled.put(column.getName(), v);
                    } else {
                        missing.put(column.getName(), td);
                    }
                } catch (ConverterException e) {
                    result.addResult(Failure.INSTANCE, context.newBlock(td.getNode(), context.getPlugin()), new PluginException("Convertion error at row: " + i + ", cell: " + j + ".", e));
                    continue;
                } catch (FormatterException e) {
                    result.addResult(Failure.INSTANCE, context.newBlock(td.getNode(), context.getPlugin()), new PluginException("Formatter error at row: " + i + ", cell: " + j + ".", e));
                    continue;
                }
            }
            if (!afilter.accept(mode, register)) {
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Register ignored[" + mode + "]:" + register + ".");
                }
                UtilNode.appendCss(row.getNode(), IDataFilter.CSS_REGISTER);
                continue;
            }
            try {
                boolean error = false;
                switch (command) {
                case INSERT:
                    if (mode == EMode.INPUT) {
                        performInsert(context, result, connection, mode, table, afilter, register, filled, missing);
                    } else {
                        performSelect(context, result, connection, table, command, register, expectedCount);
                    }
                    break;
                case UPDATE:
                    if (mode == EMode.INPUT) {
                        performUpdate(context, result, connection, table, register, expectedCount);
                    } else {
                        performSelect(context, result, connection, table, command, register, expectedCount);
                    }
                    break;
                case DELETE:
                    if (mode == EMode.INPUT) {
                        performDelete(context, result, connection, table, register, expectedCount);
                    } else {
                        performSelect(context, result, connection, table, command, register, 0);
                    }
                    break;
                default:
                    result.addResult(Failure.INSTANCE, context.newBlock(row.getNode(), context.getPlugin()), new PluginException("Invalid command type. '" + type + "' at (row:" + i + ", cell:0)"));
                    error = true;
                }
                if (!error) {
                    result.addResult(Success.INSTANCE, context.newBlock(row.getNode(), context.getPlugin()));
                }
            } catch (SQLException e) {
                if (UtilLog.LOG.isDebugEnabled()) {
                    UtilLog.LOG.debug(e.getMessage(), e);
                }
                try {
                    result.addResult(Failure.INSTANCE, context.newBlock(row.getNode(), context.getPlugin()), new PluginException("Error in connection (" + connection.getMetaData().getURL() + "): " + e.getMessage(), e));
                } catch (SQLException e1) {
                    if (UtilLog.LOG.isDebugEnabled()) {
                        UtilLog.LOG.debug(e.getMessage(), e);
                    }
                    throw new DatabaseException("Could not log error:" + e1.getMessage(), e1);
                }
            } catch (DatabaseException e) {
                if (UtilLog.LOG.isDebugEnabled()) {
                    UtilLog.LOG.debug(e.getMessage(), e);
                }
                result.addResult(Failure.INSTANCE, context.newBlock(row.getNode(), context.getPlugin()), e);
            }
        }

        if (!listeners.isEmpty()) {
            fireTableOut(new DatabaseTableEvent(context, result, adapter, this, connection, table, mode));
        }
    }

    /**
     * Recover filter from context.
     * 
     * @param context
     *            A context.
     * @param mode
     *            Database mode of action.
     * @param schema
     *            A schema.
     * @return A filter.
     * @throws DatabaseException
     *             On lookup errors.
     */
    protected IDataFilter getFilter(IContext context, EMode mode, Schema schema) throws DatabaseException {
        IDataFilter afilter = null;
        try {
            afilter = PluginFilter.getFilter(context, getFilter());
        } catch (PluginException e) {
            afilter = new DataFilterDefault();
        }
        try {
            afilter.setup(context, mode, schema);
        } catch (PluginException e) {
            throw new DatabaseException(e);
        }
        return afilter;
    }

    /**
     * Read headers information.
     * 
     * @param context
     *            The test context.
     * @param mode
     *            Database mode of action.
     * @param table
     *            The current table.
     * @param headers
     *            The headers list.
     * @param columns
     *            The columns list.
     * @param afilter
     *            A filter.
     * @throws DatabaseException
     *             On reading errors.
     */
    protected void readHeadersColumns(IContext context, EMode mode, Table table, List headers, Column[] columns, IDataFilter afilter) throws DatabaseException {
        Map found = new HashMap();
        for (int i = 0; i < headers.size(); i++) {
            CellAdapter cell = headers.get(i);
            String cAlias = cell.getValue(context);
            columns[i] = table.getAlias(cAlias);
            Column column = columns[i];
            if (i > 0 && column == null) {
                if (!UtilNode.isIgnore(cell.getNode())) {
                    throw new DatabaseException("Column '" + cAlias + "' [as '" + UtilNames.normalize(cAlias) + "'] not found in alias: " + table.getAliasToColumns().keySet() + " or in names: " + table.getNamesToColumns().keySet());
                }
            }
            // update to specific header adjusts
            if (column != null) {
                CellAdapter old = found.get(column.getAlias());
                if (old != null) {
                    throw new DatabaseException("Column with alias '" + column.getAlias() + "' repeated.");
                }
                found.put(column.getAlias(), cell);
                try {
                    UtilSchema.setupColumn(context, table, column, cell);
                } catch (ReaderException e) {
                    throw new DatabaseException(e);
                } catch (ConverterException e) {
                    throw new DatabaseException(e);
                } catch (FormatterException e) {
                    throw new DatabaseException(e);
                } catch (ComparatorException e) {
                    throw new DatabaseException(e);
                }
                if (!afilter.accept(mode, column)) {
                    if (UtilLog.LOG.isInfoEnabled()) {
                        UtilLog.LOG.info("Adding 'true' comparator to ignore a column.");
                    }
                    column.setComparator(SRServices.getComparatorManager().get("true"));
                    UtilNode.appendCss(cell.getNode(), IDataFilter.CSS_COLUMN);
                }
            }
        }
    }

    /**
     * Get the string value of a node holder, and adjust text if required.
     * 
     * @param context
     *            The context.
     * @param mode
     *            The database mode.
     * @param command
     *            Current line command.
     * @param column
     *            The column definition.
     * @param afilter
     *            A filter.
     * @param nh
     *            A node holder.
     * @return The cell as string value.
     * @throws DatabaseException
     *             On evaluation errors.
     */
    protected String getAdjustContent(IContext context, EMode mode, CommandType command, Column column, IDataFilter afilter, INodeHolder nh) throws DatabaseException {
        try {
            IReader reader = column.getReader();
            String previous = reader.read(context, nh, null);
            if (reader.isReplacer()) {
                String value = UtilExpression.replace(nh.getAttribute(INodeHolder.ATTRIBUTE_VALUE, previous), context, true);
                // if text has changed... adjust on screen.
                if (previous != null && !previous.equals(value)) {
                    nh.setValue(value);
                }
                return value;
            }
            return previous;
        } catch (ReaderException e) {
            throw new DatabaseException(e);
        } catch (PluginException e) {
            throw new DatabaseException(e);
        }
    }

    /**
     * Get value object for a given cell.
     * 
     * @param context
     *            A context.
     * @param mode
     *            Action mode.
     * @param command
     *            Command type.
     * @param column
     *            Column meta-data.
     * @param afilter
     *            A filter.
     * @param td
     *            The cell.
     * @param content
     *            Cell content.
     * @return A value, if valid, null, otherwise.
     * @throws ConverterException
     *             On conversion errors.
     * @throws DatabaseException
     *             On default value.
     * @throws FormatterException
     *             On format errors.
     */
    protected Value getValue(IContext context, EMode mode, CommandType command, Column column, IDataFilter afilter, CellAdapter td, String content) throws ConverterException, DatabaseException, FormatterException {
        boolean isNull = nullEmptyHandler.isNull(mode, content);
        boolean isEmpty = nullEmptyHandler.isEmpty(mode, content);
        boolean isVirtual = column.isVirtual();
        IConverter converter = td.getConverter(column.getConverter());
        if (isNull || isEmpty || isVirtual || converter.accept(content)) {
            Object obj = null;
            if (isNull) {
                obj = null;
            } else if (isEmpty) {
                obj = "";
            } else if (isVirtual) {
                obj = content;
            } else {
                List args = td.getArguments(column.getArguments());
                obj = converter.convert(content, args.isEmpty() ? null : args.toArray());
            }
            IFormatter formatter = td.getFormatter(column.getFormatter());
            if (formatter != null) {
                List args = td.getFormatterArguments(column.getFormatterArguments());
                obj = formatter.format(obj, args.isEmpty() ? null : args.toArray());
            }
            if (!afilter.accept(mode, column, obj)) {
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Ignore value '" + obj + "' in column '" + column.getAlias() + "'.");
                }
                UtilNode.appendCss(td.getNode(), IDataFilter.CSS_VALUE);
                return null;
            }
            // if isNull is true the user explicitly selected 'null', and so
            // keep it.
            if (!isNull && obj == null && command == CommandType.INSERT && mode == EMode.INPUT) {
                // the remaining column fields with default value are set in
                // addMissingValues(...) method.
                obj = column.getDefaultValue();
            }
            return new Value(column, td, obj, column.getComparator());
        }
        return null;
    }

    /**
     * Perform database inserts.
     * 
     * @param context
     *            The context.
     * @param result
     *            The result set.
     * @param connection
     *            The connection.
     * @param mode
     *            Database mode of action.
     * @param table
     *            The specification.
     * @param afilter
     *            A filter.
     * @param register
     *            The values.
     * @param filled
     *            Filled fields.
     * @param missing
     *            Unfilled columns.
     * @throws DatabaseException
     *             On database errors.
     * @throws SQLException
     *             On SQL errors.
     */
    protected void performInsert(IContext context, IResultSet result, Connection connection, EMode mode, Table table, IDataFilter afilter, IRegister register, Map filled, Map missing) throws DatabaseException, SQLException {
        addMissingValues(mode, table, afilter, register, filled, missing);
        performIn(context, result, connection, sqlWrapperFactory.createInputWrapper(table, CommandType.INSERT, register, 1), table, register);
    }

    /**
     * Add missing values to insert value set.
     * 
     * @param mode
     *            Database mode of action.
     * @param table
     *            The table.
     * @param afilter
     *            A filter.
     * @param register
     *            The set of values.
     * @param filled
     *            A map of filled fields.
     * @param missing
     *            A map of unfilled fields.
     * @throws DatabaseException
     *             On default value construction error.
     */
    protected void addMissingValues(EMode mode, Table table, IDataFilter afilter, IRegister register, Map filled, Map missing) throws DatabaseException {
        for (Column column : table.getColumns()) {
            // table columns not present in test table
            if (filled.get(column.getName()) == null) {
                Value v = null;
                Object defaultValue = column.getDefaultValue();
                if (defaultValue != null) {
                    // with default values should be set
                    v = new Value(column, missing.get(column.getName()), defaultValue, column.getComparator());
                } else if (column.isSequence()) {
                    // or if it is a sequence: add their next value command.
                    v = new Value(column, missing.get(column.getName()), sequenceProvider.nextValue(column.getSequence()), column.getComparator());
                }
                if (v != null) {
                    if (!afilter.accept(mode, column) || !afilter.accept(mode, column, null) || !afilter.accept(mode, column, v.getValue())) {
                        if (UtilLog.LOG.isInfoEnabled()) {
                            UtilLog.LOG.info("Ignore default of ignored column '" + column.getAlias() + "(" + column.getName() + ")'.");
                        }
                        if (v.getCell() != null) {
                            UtilNode.appendCss(v.getCell().getNode(), IDataFilter.CSS_VALUE);
                        }
                        continue;
                    }
                    register.add(v);
                }
            }
        }
    }

    /**
     * Perform database updates.
     * 
     * @param context
     *            The context.
     * @param result
     *            The result set.
     * @param connection
     *            The connection.
     * @param table
     *            The specification.
     * @param register
     *            The values.
     * @param expectedCount
     *            The expected action counter.
     * @throws DatabaseException
     *             On database errors.
     * @throws SQLException
     *             On SQL errors.
     */
    protected void performUpdate(IContext context, IResultSet result, Connection connection, Table table, IRegister register, int expectedCount) throws DatabaseException, SQLException {
        performIn(context, result, connection, sqlWrapperFactory.createInputWrapper(table, CommandType.UPDATE, register, expectedCount), table, register);
    }

    /**
     * Perform database deletes.
     * 
     * @param context
     *            The context.
     * @param result
     *            The result set.
     * @param connection
     *            The connection.
     * @param table
     *            The specification.
     * @param register
     *            The values.
     * @param expectedCount
     *            The delete expected count.
     * @throws DatabaseException
     *             On database errors.
     * @throws SQLException
     *             On SQL errors.
     */
    protected void performDelete(IContext context, IResultSet result, Connection connection, Table table, IRegister register, int expectedCount) throws DatabaseException, SQLException {
        performIn(context, result, connection, sqlWrapperFactory.createInputWrapper(table, CommandType.DELETE, register, expectedCount), table, register);
    }

    /**
     * Perform database commands.
     * 
     * @param context
     *            The context.
     * @param result
     *            The result set.
     * @param connection
     *            The connection.
     * @param wrapper
     *            The SQL wrapper.
     * @param table
     *            The target table.
     * @param register
     *            The values.
     * @throws DatabaseException
     *             On database errors.
     * @throws SQLException
     *             On SQL errors.
     */
    protected void performIn(IContext context, IResultSet result, Connection connection, SqlWrapper wrapper, Table table, IRegister register) throws DatabaseException, SQLException {
        Map namesToIndexes = wrapper.getNamesToIndexes();
        if (UtilLog.LOG.isDebugEnabled()) {
            UtilLog.LOG.debug(wrapper.getSql() + ". MAP: " + namesToIndexes + ". VALUES: " + register);
        }
        idManager.clear();

        PreparedStatement pstmt = null;
        try {
            pstmt = statementFactory.getInput(connection, wrapper.getSql(), table);
            Map indexesToValues = prepareInputValues(context, table, register, namesToIndexes);
            for (Entry e : indexesToValues.entrySet()) {
                pstmt.setObject(e.getKey(), e.getValue());
            }

            int count = pstmt.executeUpdate();
            if (UtilLog.LOG.isDebugEnabled()) {
                UtilLog.LOG.debug("[" + count + "]=" + wrapper.getSql());
            }
            if (wrapper.getExpectedCount() != Integer.MAX_VALUE && wrapper.getExpectedCount() != count) {
                throw new DatabaseException("The expected count (" + wrapper.getExpectedCount() + ") does not match, received = " + count + ".\n\tSQL: " + wrapper.getSql() + "\n\tARGS: " + register);
            }

            DatabaseMetaData meta = connection.getMetaData();
            if (idManager.hasKeys() && meta.supportsGetGeneratedKeys()) {
                idManager.readKeys(wrapper, table, register, pstmt);
            }

            if (!listeners.isEmpty()) {
                fireRegisterIn(new DatabaseRegisterEvent(context, result, this, connection, table, register, wrapper, indexesToValues));
            }
        } finally {
            if (pstmt != null) {
                statementFactory.release(pstmt);
            }
        }
    }

    /**
     * Prepare values to use in insert/update/delete.
     * 
     * @param context
     *            The test alias.
     * @param table
     *            A table.
     * @param register
     *            A register.
     * @param namesToIndexes
     *            Map from indexes to values.
     * @return A map of object to use in statement setup.
     * @throws DatabaseException
     *             On prepare errors.
     */
    protected Map prepareInputValues(IContext context, Table table, IRegister register, Map namesToIndexes) throws DatabaseException {
        Map indexesToValues = new HashMap();
        for (Value v : register) {
            Column column = v.getColumn();
            Integer index = namesToIndexes.get(column.getName());
            if (index != null) {
                String tableOrAlias = register.getTableOrAlias(context, column);
                Object obj = v.getValue();
                if (column.isVirtual()) {
                    obj = idManager.lookup(tableOrAlias, String.valueOf(obj));
                }
                if (column.isReference()) {
                    idManager.append(table.getAlias(), v.getCell().getValue(context));
                }
                if (UtilLog.LOG.isDebugEnabled()) {
                    UtilLog.LOG.debug("performIn.SET(" + index + "," + column.getName() + ") = " + obj);
                }
                indexesToValues.put(index, obj);
            }
        }
        return indexesToValues;
    }

    /**
     * Perform database select verifications.
     * 
     * @param context
     *            The context.
     * @param result
     *            The result set.
     * @param connection
     *            The connection.
     * @param table
     *            The specification.
     * @param command
     *            Command type.
     * @param register
     *            The values.
     * @param expectedCount
     *            The select expected count.
     * @throws DatabaseException
     *             On database errors.
     * @throws SQLException
     *             On SQL errors.
     */
    protected void performSelect(IContext context, IResultSet result, Connection connection, Table table, CommandType command, IRegister register, int expectedCount) throws DatabaseException, SQLException {
        performOut(context, result, connection, sqlWrapperFactory.createOutputWrapper(table, command, register, expectedCount), table, register);
    }

    /**
     * Perform database selects.
     * 
     * @param context
     *            The context.
     * @param result
     *            The result set.
     * @param connection
     *            The connection.
     * @param wrapper
     *            The SQL wrapper.
     * @param table
     *            The output table.
     * @param register
     *            The values.
     * @throws DatabaseException
     *             On database errors.
     * @throws SQLException
     *             On SQL errors.
     */
    protected void performOut(IContext context, IResultSet result, Connection connection, SqlWrapper wrapper, Table table, IRegister register) throws DatabaseException, SQLException {
        Map namesToIndexes = wrapper.getNamesToIndexes();
        String sql = wrapper.getSql();
        if (UtilLog.LOG.isDebugEnabled()) {
            UtilLog.LOG.debug(sql + ". MAP:" + namesToIndexes + ". values = " + register + ". indexes = " + namesToIndexes);
        }
        PreparedStatement pstmt = null;
        try {
            pstmt = statementFactory.getOutput(connection, wrapper.getSql(), table);
            Map indexesToValues = prepareSelectValues(context, connection, register, namesToIndexes);
            for (Entry e : indexesToValues.entrySet()) {
                pstmt.setObject(e.getKey(), e.getValue());
            }
            ResultSet rs = null;
            try {
                rs = pstmt.executeQuery();
                if (wrapper.getExpectedCount() == 1) {
                    if (!rs.next()) {
                        throw new DatabaseException("None register found with the given conditions: " + sql + " and values: [" + register + "]");
                    }
                    compareRegister(context, result, connection, register, namesToIndexes, rs);
                    if (rs.next()) {
                        throw new DatabaseException("More than one register satisfy the condition: " + sql + "[" + register + "]\n" + dumpRs("Extra itens:", rs));
                    }
                } else {
                    if (rs.next()) {
                        throw new DatabaseException("A result for " + sql + "[" + register + "] was not expected.\n" + dumpRs("Unexpected items:", rs));
                    }
                }
            } finally {
                if (rs != null) {
                    rs.close();
                }
            }
            if (!listeners.isEmpty()) {
                fireRegisterOut(new DatabaseRegisterEvent(context, result, this, connection, table, register, wrapper, indexesToValues));
            }
        } finally {
            if (pstmt != null) {
                statementFactory.release(pstmt);
            }
        }
    }

    /**
     * Prepare values to use in select.
     * 
     * @param connection
     *            A connection.
     * @param register
     *            A register.
     * @param namesToIndexes
     *            Map from indexes to values.
     * @return A map of object to use in statement setup.
     * @throws DatabaseException
     *             On prepare errors.
     * @throws SQLException
     *             On database errors.
     */
    protected Map prepareSelectValues(IContext context, Connection connection, IRegister register, Map namesToIndexes) throws DatabaseException, SQLException {
        Map indexesToValues = new HashMap();
        for (Value v : register) {
            Column column = v.getColumn();
            Integer index = namesToIndexes.get(column.getName());
            if (index != null) {
                Object value = v.getValue();
                if (column.isVirtual()) {
                    value = idManager.find(register.getTableOrAlias(context, column), String.valueOf(value), column, connection, statementFactory);
                }
                if (column.isDate()) {
                    IComparator comp = column.getComparator();
                    if (!(comp instanceof ComparatorDate)) {
                        throw new DatabaseException("Date columns must have comparators of type 'date'. Current type:" + comp.getClass());
                    }
                    ComparatorDate comparator = (ComparatorDate) comp;
                    comparator.initialize();
                    Date dateBefore = new Date(((Date) value).getTime() - comparator.getTolerance());
                    Date dateAfter = new Date(((Date) value).getTime() + comparator.getTolerance());
                    if (UtilLog.LOG.isDebugEnabled()) {
                        UtilLog.LOG.debug("performOut.SET(" + (index) + "," + column.getAlias() + "," + column.getName() + ") = " + dateBefore);
                        UtilLog.LOG.debug("performOut.SET(" + (index + 1) + "," + column.getAlias() + "," + column.getName() + ") = " + dateAfter);
                    }
                    indexesToValues.put(index, dateBefore);
                    indexesToValues.put(index + 1, dateAfter);
                } else {
                    if (UtilLog.LOG.isDebugEnabled()) {
                        UtilLog.LOG.debug("performOut.SET(" + index + "," + column.getAlias() + "," + column.getName() + ") = " + value);
                    }
                    indexesToValues.put(index, value);
                }
                v.setValue(value);
            }
        }
        return indexesToValues;
    }

    /**
     * Compare register with current result set item.
     * 
     * @param context
     *            The test context.
     * @param result
     *            The result.
     * @param connection
     *            The connection.
     * @param register
     *            The register.
     * @param namesToIndexes
     *            A mapping from column names to indexes.
     * @param rs
     *            A result.
     * @throws DatabaseException
     *             On compare errors.
     * @throws SQLException
     *             On database errors.
     */
    protected void compareRegister(IContext context, IResultSet result, Connection connection, IRegister register, Map namesToIndexes, ResultSet rs) throws DatabaseException, SQLException {
        for (Value v : register) {
            Column column = v.getColumn();
            Integer index = namesToIndexes.get(column.getName());
            if (index == null) {
                IComparator comparator = v.getComparator();
                Object received = columnReader.read(rs, column);
                if (UtilLog.LOG.isDebugEnabled()) {
                    UtilLog.LOG.debug("CHECK(" + v.getValue() + ") = " + received);
                }
                Object value = v.getValue();
                String tableOrAlias = register.getTableOrAlias(context, column);
                if (column.isVirtual()) {
                    value = idManager.find(register.getTableOrAlias(context, column), String.valueOf(value), column, connection, statementFactory);
                }
                CellAdapter cell = v.getCell();
                comparator.initialize();
                try {
                    boolean notMatch = !comparator.match(value, received);
                    if (notMatch) {
                        Object expected = v.getValue();
                        if (column.isVirtual()) {
                            received = idManager.lookup(tableOrAlias, String.valueOf(received));
                        }
                        String expStr = UtilSql.toString(value(v.getColumn(), expected));
                        String recStr = UtilSql.toString(value(v.getColumn(), received));
                        shortView(cell, expected, expStr, recStr);
                        IPresentation error = null;
                        if (expStr.equals(recStr)) {
                            // same string representation but different object
                            // types
                            error = new PresentationCompare(expected, received);
                        } else {
                            // equal object types.
                            error = new DefaultAlignmentException("Values are different.", expStr, recStr);
                        }
                        result.addResult(Failure.INSTANCE, context.newBlock(cell.getNode(), context.getPlugin()), new PresentationException(error));
                    } else {
                        String str = String.valueOf(value);
                        if (!str.equals(cell.getValue(context))) {
                            cell.append(" {" + str + "}");
                        }
                    }
                } catch (ComparatorException e) {
                    result.addResult(Failure.INSTANCE, context.newBlock(cell.getNode(), context.getPlugin()), e);
                }
            }
        }
    }

    /**
     * Recover better representation for a value.
     * 
     * @param c
     *            A column.
     * @param exp
     *            A value.
     * @return Object representation.
     */
    protected Object value(Column c, Object exp) {
        Object out = exp;
        if (c.getConverter() instanceof IConverterReverse) {
            IConverterReverse converter = (IConverterReverse) c.getConverter();
            List arguments = c.getArguments();
            try {
                out = converter.revert(exp, arguments.toArray(new Object[arguments.size()]));
            } catch (ConverterException e) {
                if (UtilLog.LOG.isTraceEnabled()) {
                    UtilLog.LOG.trace("Unable to revert '" + exp + "' with: " + converter + "" + arguments);
                }
            }
        }
        return out;
    }

    /**
     * Create an error information to append to a cell.
     * 
     * @param cell
     *            A cell.
     * @param expected
     *            Expected object.
     * @param expStr
     *            Expected as string.
     * @param recStr
     *            Received as string.
     */
    protected void shortView(CellAdapter cell, Object expected, String expStr, String recStr) {
        if (!expStr.equals(expected)) {
            Element spanExp = new Element("span");
            spanExp.addAttribute(new Attribute("class", "compare"));
            spanExp.appendChild(new Element("br"));
            {
                Element spanLabelExp = new Element("span");
                spanLabelExp.addAttribute(new Attribute("class", "expected"));
                spanExp.appendChild(spanLabelExp);

                spanLabelExp.appendChild(" (expected):");
            }
            spanExp.appendChild(expStr);
            cell.append(spanExp);
        }
        Element spanRec = new Element("span");
        spanRec.addAttribute(new Attribute("class", "compare"));
        spanRec.appendChild(new Element("br"));
        {
            Element spanLabelRec = new Element("span");
            spanLabelRec.addAttribute(new Attribute("class", "received"));
            spanRec.appendChild(spanLabelRec);

            spanLabelRec.appendChild(" (received):");
        }
        spanRec.appendChild(recStr);
        cell.append(spanRec);
    }

    /**
     * Dump result set.
     * 
     * @param prefix
     *            The message prefix.
     * @param rs
     *            The result set.
     * @return A string for result set.
     * @throws SQLException
     *             On reading errors.
     */
    protected String dumpRs(String prefix, ResultSet rs) throws SQLException {
        StringBuilder sb = new StringBuilder(prefix);
        ResultSetMetaData meta = rs.getMetaData();
        int count = meta.getColumnCount();
        int index = 0;
        do {
            sb.append("\n");
            for (int i = 1; i <= count; i++) {
                sb.append((i == 1 ? "\t" : ", ") + meta.getColumnName(i) + ":" + rs.getObject(i));
            }
            index++;
        } while (index < limit && rs.next());
        return sb.toString();
    }

    @Override
    public void release() throws PluginException {
        statementFactory.release();
    }

    @Override
    public Object getObject() {
        return this;
    }

    @Override
    public void destroy() {
        xmlToSql.release();
        if (UtilLog.LOG.isInfoEnabled()) {
            UtilLog.LOG.info("Cache of scripts released: " + xmlToSql);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy