org.jumpmind.symmetric.io.data.DbFill.java Source code

Java tutorial

Introduction

Here is the source code for org.jumpmind.symmetric.io.data.DbFill.java

Source

/**
 * Licensed to JumpMind Inc under one or more contributor
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU General Public License, version 3.0 (GPLv3)
 * (the "License"); you may not use this file except in compliance
 * with the License.
 *
 * You should have received a copy of the GNU General Public License,
 * version 3.0 (GPLv3) along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jumpmind.symmetric.io.data;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.jumpmind.db.model.Column;
import org.jumpmind.db.model.ForeignKey;
import org.jumpmind.db.model.Reference;
import org.jumpmind.db.model.Table;
import org.jumpmind.db.platform.IDatabasePlatform;
import org.jumpmind.db.sql.DmlStatement;
import org.jumpmind.db.sql.DmlStatement.DmlType;
import org.jumpmind.db.sql.ISqlRowMapper;
import org.jumpmind.db.sql.Row;
import org.jumpmind.db.sql.SqlException;
import org.jumpmind.db.util.BinaryEncoding;
import org.jumpmind.util.AppUtils;
import org.jumpmind.util.FormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Generate data for populating databases.
 */
public class DbFill {

    final Logger log = LoggerFactory.getLogger(getClass());

    private String catalog;

    private String schema;

    private IDatabasePlatform platform;

    private boolean ignoreMissingTables;

    private boolean cascading = false;

    private String ignore[] = null;

    private String prefixed[] = null;

    private int inputLength = 1;

    private Random rand = null;

    private int interval = 0;

    private boolean debug = false;

    private boolean verbose = false;

    private boolean continueOnError = false;

    private boolean print = false;

    private String textColumnExpression;

    // Weights given to insert, update, and delete commands when
    // randomly selecting a command for any given table.
    private int[] dmlWeight = { 1, 0, 0 };

    private Table[] allDbTablesCache = null;

    // -1 for no limit
    private static final int RANDOM_SELECT_SIZE = 100;

    // Must remain 0-2 to choose randomly.
    public final static int INSERT = 0;
    public final static int UPDATE = 1;
    public final static int DELETE = 2;

    private boolean firstPass = true;

    public DbFill() {
    }

    public DbFill(IDatabasePlatform platform) {
        this.platform = platform;
    }

    public void fillTables(String... tableNames) {
        fillTables(tableNames, null);
    }

    public void fillTables(String[] tableNames, Map<String, int[]> tableProperties) {
        Table[] tables;
        if (tableNames.length == 0) {
            // If no tableNames are provided look up all tables.
            tables = getAllDbTables();
            if (ignore != null) {
                // Ignore any tables matching an ignorePrefix. (e.g., "sym_")
                List<Table> tableList = new ArrayList<Table>(tables.length);
                table_loop: for (Table table : tables) {
                    for (String ignoreName : ignore) {
                        if (table.getName().startsWith(ignoreName)) {
                            if (verbose) {
                                log.info("Ignore table " + table.getName());
                            }
                            continue table_loop;
                        }
                    }
                    for (String prefixedName : prefixed) {
                        if (!table.getName().startsWith(prefixedName)) {
                            if (verbose) {
                                log.info("Non prefixed table (" + prefixedName + ")" + table.getName());
                            }
                            continue table_loop;
                        }
                    }
                    tableList.add(table);
                }
                tables = tableList.toArray(new Table[tableList.size()]);
            }
        } else {
            ArrayList<Table> tableList = new ArrayList<Table>();
            for (String tableName : tableNames) {
                Table table = platform.readTableFromDatabase(getCatalogToUse(), getSchemaToUse(), tableName);
                if (table != null) {
                    tableList.add(table);
                } else if (!ignoreMissingTables) {
                    throw new RuntimeException("Cannot find table " + tableName + " in catalog " + getCatalogToUse()
                            + " and schema " + getSchemaToUse());
                }
            }
            tables = tableList.toArray(new Table[tableList.size()]);
        }

        fillTables(tables, tableProperties);
    }

    /**
     * Identify the tables not included in the given list that the initial tables have FK relationships to.
     *
     * @param tables
     *
     * @return The table array argument and the tables that the initial table array argument depend on.
     */
    public Table[] addFkInsertDependentTables(Table... tables) {
        Table[] fkDepTblArray = null;
        if (tables != null) {
            List<Table> fkDepList = new ArrayList<Table>();
            Set<String> tableNames = new HashSet<String>();
            for (Table tbl : tables) {
                tableNames.add(tbl.getName());
            }
            for (Table table : tables) {
                for (ForeignKey fk : table.getForeignKeys()) {
                    if (tableNames.add(fk.getForeignTableName())) {
                        Table tableObj = getDbTable(fk.getForeignTableName());
                        fkDepList.add(tableObj);
                    }
                }
            }
            fkDepTblArray = fkDepList.toArray(new Table[fkDepList.size()]);
            fkDepTblArray = (Table[]) ArrayUtils.addAll(fkDepTblArray, tables);
            if (fkDepList.size() > 0) {
                fkDepTblArray = addFkInsertDependentTables(fkDepTblArray);
            }
        }
        return fkDepTblArray;
    }

    /**
     * Identify the tables not included in the given list that the initial tables have FK relationships to.
     *
     * @param deleteTables
     *
     * @return The table array argument and the tables that the initial table array argument depend on.
     */
    public Table[] addFkDeleteDependentTables(Table... deleteTables) {

        Table[] fkDepTblArray = null;
        if (deleteTables != null) {
            List<Table> fkDepList = new ArrayList<Table>();
            Set<String> deleteTableNames = new HashSet<String>();
            for (Table tbl : deleteTables) {
                deleteTableNames.add(tbl.getName());
            }
            Table[] allTables = getAllDbTables();
            for (Table table : allTables) {
                for (ForeignKey fk : table.getForeignKeys()) {
                    if (deleteTableNames.contains(fk.getForeignTableName())) {
                        if (deleteTableNames.add(table.getName())) {
                            fkDepList.add(table);
                        }
                    }
                }
            }
            fkDepTblArray = fkDepList.toArray(new Table[fkDepList.size()]);
            fkDepTblArray = (Table[]) ArrayUtils.addAll(fkDepTblArray, deleteTables);
            if (fkDepList.size() > 0) {
                fkDepTblArray = addFkDeleteDependentTables(fkDepTblArray);
            }
        }
        return fkDepTblArray;
    }

    /**
     * Once we have an array of table objects we can begin sorting and IUD operations.
     *
     * @param tables Array of table objects.
     */
    private void fillTables(Table[] tables, Map<String, int[]> tableProperties) {
        for (int i = 0; i < inputLength; i++) {
            makePass(tables, tableProperties);
        }
    }

    /**
     * Perform an INSERT, UPDATE, or DELETE on every table in tables.
     *
     * @param tables Array of tables to perform statement on. Tables must be in
     *          insert order.
     * @param tableProperties Map indicating IUD weights for each table name provided
     *          in the properties file.
     */
    private void makePass(Table[] tables, Map<String, int[]> tableProperties) {
        for (Table table : tables) {
            // Sleep for the configured time between tables
            if (!firstPass) {
                AppUtils.sleep(interval);
            } else {
                firstPass = false;
            }
            int dmlType = INSERT;
            if (tableProperties != null && tableProperties.containsKey(table.getName())) {
                dmlType = randomIUD(tableProperties.get(table.getName()));
            } else if (dmlWeight != null) {
                dmlType = randomIUD(dmlWeight);
            }
            switch (dmlType) {
            case INSERT:
                if (verbose) {
                    log.info("Inserting into table " + table.getName());
                }
                insertRandomRecord(table);
                break;
            case UPDATE:
                if (verbose) {
                    log.info("Updating record in table " + table.getName());
                }
                updateRandomRecord(table);
                break;
            case DELETE:
                if (verbose) {
                    log.info("Deleting record in table " + table.getName());
                }
                deleteRandomRecord(table);
                break;
            }
        }
    }

    /**
     * Given a table's IUD weights a random DML statement type is chosen.
     *
     * @param iudWeight
     * @return
     */
    private int randomIUD(int[] iudWeight) {
        if (iudWeight.length != 3) {
            throw new RuntimeException("Incorrect number of IUD weights provided.");
        }
        int total = iudWeight[0] + iudWeight[1] + iudWeight[2];
        if (total == 0) {
            return INSERT;
        }
        int rVal = getRand().nextInt(total);
        if (rVal < iudWeight[0]) {
            return INSERT;
        } else if (rVal < iudWeight[0] + iudWeight[1]) {
            return UPDATE;
        }
        return DELETE;
    }

    /**
     * Select a random row from the table in the connected database. Return null if there are no rows.
     *
     * TODO: Cache rows.
     *
     * @param sqlTemplate
     * @param table The table to select a row from.
     * @return A random row from the table. Null if there are no rows.
     */
    private Row selectRandomRow(Table table) {
        Row row = null;
        // Select all rows and return the primary key columns.
        String sql = platform.createDmlStatement(DmlType.SELECT_ALL, table.getCatalog(), table.getSchema(),
                table.getName(), table.getPrimaryKeyColumns(), table.getColumns(), null, textColumnExpression)
                .getSql();
        final List<Row> rows = new ArrayList<Row>();
        platform.getSqlTemplate().query(sql, RANDOM_SELECT_SIZE, new ISqlRowMapper<Object>() {
            public Object mapRow(Row row) {
                rows.add(row);
                return Boolean.TRUE;
            }
        }, null, null);
        if (rows.size() != 0) {
            int rowNum = getRand().nextInt(rows.size());
            row = rows.get(rowNum);
        }
        return row;
    }

    private void updateRandomRecord(Table table) {
        DmlStatement updStatement = createUpdateDmlStatement(table);
        Row row = createRandomUpdateValues(updStatement, table);
        try {
            platform.getSqlTemplate().update(updStatement.getSql(), row.toArray(table.getColumnNames()));
            if (verbose) {
                log.info("Successful update in " + table.getName());
            }
        } catch (SqlException ex) {
            log.info("Failed to process {} with values of {}", updStatement.getSql(),
                    ArrayUtils.toString(row.toArray(table.getColumnNames())));
            if (continueOnError) {
                if (debug) {
                    log.info("", ex);
                }
            } else {
                throw ex;
            }
        }
    }

    /**
      * Select a random row from the table and update all columns except for primary and foreign keys.
      *
      * @param sqlTemplate
      * @param table
      */
    private void insertRandomRecord(Table table) {
        DmlStatement insertStatement = createInsertDmlStatement(table);
        Row row = createRandomInsertValues(insertStatement, table);
        try {
            platform.getSqlTemplate().update(insertStatement.getSql(), insertStatement.getValueArray(
                    row.toArray(table.getColumnNames()), row.toArray(table.getPrimaryKeyColumnNames())));
            if (verbose) {
                log.info("Successful update in " + table.getName());
            }
        } catch (SqlException ex) {
            log.info("Failed to process {} with values of {}", insertStatement.getSql(),
                    ArrayUtils.toString(row.toArray(table.getColumnNames())));
            if (continueOnError) {
                if (debug) {
                    log.info("", ex);
                }
            } else {
                throw ex;
            }
        }
    }

    public String createDynamicRandomInsertSql(Table table) {
        DmlStatement insertStatement = createInsertDmlStatement(table);
        Row row = createRandomInsertValues(insertStatement, table);
        return insertStatement.buildDynamicSql(BinaryEncoding.HEX, row, false, true);
    }

    public String createDynamicRandomUpdateSql(Table table) {
        DmlStatement updStatement = createUpdateDmlStatement(table);
        Row row = createRandomUpdateValues(updStatement, table);
        return updStatement.buildDynamicSql(BinaryEncoding.HEX, row, false, true);
    }

    public String createDynamicRandomDeleteSql(Table table) {
        DmlStatement deleteStatement = createDeleteDmlStatement(table);
        Row row = selectRandomRow(table);
        return deleteStatement.buildDynamicDeleteSql(BinaryEncoding.HEX, row, false, true);
    }

    /**
     * Delete a random row in the given table or delete all rows matching selectColumns
     * in the given table.
     *
     * @param table Table to delete from.
     * @param selectColumns If provided, the rows that match this criteria are deleted.
     */
    private void deleteRandomRecord(Table table) {
        DmlStatement deleteStatement = createDeleteDmlStatement(table);
        Row row = selectRandomRow(table);
        try {
            platform.getSqlTemplate().update(deleteStatement.getSql(), row.toArray(table.getColumnNames()));
            if (verbose) {
                log.info("Successful update in " + table.getName());
            }
        } catch (SqlException ex) {
            log.info("Failed to process {} with values of {}", deleteStatement.getSql(),
                    ArrayUtils.toString(row.toArray(table.getColumnNames())));
            if (continueOnError) {
                if (debug) {
                    log.info("", ex);
                }
            } else {
                throw ex;
            }
        }
    }

    private Object generateRandomValueForColumn(Column column) {
        Object objectValue = null;
        int type = column.getMappedTypeCode();
        if (column.isEnum()) {
            objectValue = column.getEnumValues()[new Random().nextInt(column.getEnumValues().length)];
        } else if (column.isTimestampWithTimezone()) {
            objectValue = String.format("%s %s", FormatUtils.TIMESTAMP_FORMATTER.format(randomDate()),
                    AppUtils.getTimezoneOffset());
        } else if (type == Types.DATE) {
            objectValue = DateUtils.truncate(randomDate(), Calendar.DATE);
        } else if (type == Types.TIMESTAMP || type == Types.TIME) {
            objectValue = randomTimestamp();
        } else if (type == Types.INTEGER || type == Types.BIGINT) {
            objectValue = randomInt();
        } else if (type == Types.SMALLINT) {
            objectValue = randomSmallInt(column.getJdbcTypeName().toLowerCase().contains("unsigned"));
        } else if (type == Types.FLOAT) {
            objectValue = randomFloat();
        } else if (type == Types.DOUBLE) {
            objectValue = randomDouble();
        } else if (type == Types.TINYINT) {
            objectValue = randomTinyInt();
        } else if (type == Types.NUMERIC || type == Types.DECIMAL || type == Types.REAL) {
            objectValue = randomBigDecimal(column.getSizeAsInt(), column.getScale());
        } else if (type == Types.BOOLEAN || type == Types.BIT) {
            objectValue = randomBoolean();
        } else if (type == Types.BLOB || type == Types.LONGVARBINARY || type == Types.BINARY
                || type == Types.VARBINARY ||
                // SQLServer text type
                type == -10) {
            objectValue = randomBytes();
        } else if (type == Types.ARRAY) {
            objectValue = null;
        } else if (type == Types.VARCHAR || type == Types.LONGVARCHAR || type == Types.CHAR || type == Types.CLOB) {
            int size = 0;
            // Assume if the size is 0 there is no max size configured.
            if (column.getSizeAsInt() != 0) {
                size = column.getSizeAsInt() > 50 ? 50 : column.getSizeAsInt();
            } else {
                // No max length so default to 50
                size = 50;
            }
            objectValue = randomString(size);
        } else if (type == Types.OTHER) {
            if ("UUID".equalsIgnoreCase(column.getJdbcTypeName())) {
                objectValue = randomUUID();
            }
        }
        return objectValue;
    }

    private Object randomSmallInt(boolean unsigned) {
        if (unsigned) {
            return new Integer(getRand().nextInt(32768));
        } else {
            // TINYINT (-32768 32767)
            return new Integer(getRand().nextInt(65535) - 32768);
        }
    }

    private Object randomFloat() {
        return getRand().nextFloat();
    }

    private Object randomDouble() {
        final long places = 1000000000l;
        double d = Math.random() * places;
        long l = Math.round(d);
        return ((double) l) / (double) places + 2 + (double) randomInt();
    }

    private Object randomTinyInt() {
        // TINYINT (-128 to 127) or (0 to 255) depending on database platform
        return new Integer(getRand().nextInt(127));
    }

    private String randomString(int maxLength) {
        StringBuilder str = new StringBuilder(maxLength);
        for (int i = 0; i < maxLength; i++) {
            str.append(randomChar());
        }
        return str.toString();
    }

    private byte[] randomBytes() {
        int length = 10;
        byte array[] = new byte[length];
        for (int i = 0; i < length; i++) {
            array[i] = (byte) getRand().nextInt(256);
        }
        return array;
    }

    private boolean randomBoolean() {
        return getRand().nextBoolean();
    }

    private BigDecimal randomBigDecimal(int size, int digits) {
        if (size == 0 && digits == 0) {
            // set the values to something reasonable
            size = 10;
            digits = 6;
        }
        Random rnd = getRand();
        StringBuilder str = new StringBuilder();
        if (size > 0 && rnd.nextBoolean()) {
            str.append("-");
        }
        for (int i = 0; i < size; i++) {
            if (i == size - digits)
                str.append(".");
            str.append(rnd.nextInt(10));
        }
        return new BigDecimal(str.toString());
    }

    private Character randomChar() {
        int rnd = getRand().nextInt(52);
        char base = (rnd < 26) ? 'A' : 'a';
        return (char) (base + rnd % 26);
    }

    private Date randomDate() {
        // Random date between 1970 and 2020
        long l = Math.abs(getRand().nextLong());
        long ms = (50L * 365 * 24 * 60 * 60 * 1000);
        return new Date(l % ms);
    }

    private Timestamp randomTimestamp() {
        return Timestamp.valueOf(FormatUtils.TIMESTAMP_FORMATTER.format(randomDate()));
    }

    private Integer randomInt() {
        return new Integer(getRand().nextInt(1000000));
    }

    private String randomUUID() {
        return UUID.randomUUID().toString();
    }

    public String getSchemaToUse() {
        if (StringUtils.isBlank(schema)) {
            return platform.getDefaultSchema();
        } else {
            return schema;
        }
    }

    public String getCatalogToUse() {
        if (StringUtils.isBlank(catalog)) {
            return platform.getDefaultCatalog();
        } else {
            return catalog;
        }
    }

    protected List<String> getLocalFkRefColumns(Table table) {
        List<String> columns = new ArrayList<String>();
        for (ForeignKey fk : table.getForeignKeys()) {
            for (Reference ref : fk.getReferences()) {
                columns.add(ref.getLocalColumnName());
            }
        }
        return columns;
    }

    protected Table[] getAllDbTables() {
        if (allDbTablesCache == null) {
            allDbTablesCache = platform.readDatabase(getCatalogToUse(), getSchemaToUse(), null).getTables();
        }
        return allDbTablesCache;
    }

    protected Table getDbTable(String tableName) {
        if (allDbTablesCache == null) {
            allDbTablesCache = platform.readDatabase(getCatalogToUse(), getSchemaToUse(), null).getTables();
        }
        for (Table table : allDbTablesCache) {
            if (table.getName().equalsIgnoreCase(tableName)) {
                return table;
            }
        }
        return null;
    }

    public DmlStatement createInsertDmlStatement(Table table) {
        return platform.createDmlStatement(DmlType.INSERT, table.getCatalog(), table.getSchema(), table.getName(),
                table.getPrimaryKeyColumns(), table.getColumns(), null, textColumnExpression);
    }

    public DmlStatement createUpdateDmlStatement(Table table) {
        return platform.createDmlStatement(DmlType.UPDATE, table.getCatalog(), table.getSchema(), table.getName(),
                table.getPrimaryKeyColumns(), table.getNonPrimaryKeyColumns(), null, textColumnExpression);
    }

    public DmlStatement createDeleteDmlStatement(Table table) {
        return platform.createDmlStatement(DmlType.DELETE, table.getCatalog(), table.getSchema(), table.getName(),
                table.getPrimaryKeyColumns(), table.getNonPrimaryKeyColumns(), null, textColumnExpression);
    }

    private Row createRandomInsertValues(DmlStatement updStatement, Table table) {
        Column[] columns = updStatement.getMetaData();
        Row row = new Row(columns.length);
        for (int i = 0; i < columns.length; i++) {
            row.put(columns[i].getName(), generateRandomValueForColumn(columns[i]));
        }
        return row;
    }

    private Row createRandomUpdateValues(DmlStatement updStatement, Table table) {
        Row row = selectRandomRow(table);
        if (row == null) {
            log.warn("Unable to update a random record in empty table '" + table.getName() + "'.");
            return null;
        }
        Column[] columns = updStatement.getMetaData();

        // Get list of local fk reference columns
        List<String> localFkRefColumns = getLocalFkRefColumns(table);
        for (int i = 0; i < columns.length; i++) {
            if (!(columns[i].isPrimaryKey() || localFkRefColumns.contains(columns[i].getName()))) {
                row.put(columns[i].getName(), generateRandomValueForColumn(columns[i]));
            }
        }
        return row;
    }

    protected String getTypeValue(String type, String value) {
        if (type.equalsIgnoreCase("CHAR")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("VARCHAR")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("LONGVARCHAR")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("DATE")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("TIME")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("TIMESTAMP")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("CLOB")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("BLOB")) {
            value = "'" + value + "'";
        } else if (type.equalsIgnoreCase("ARRAY")) {
            value = "[" + value + "]";
        }
        return value;
    }

    public void setPlatform(IDatabasePlatform platform) {
        this.platform = platform;
    }

    public int getRecordCount() {
        return inputLength;
    }

    public void setRecordCount(int recordCount) {
        this.inputLength = recordCount;
    }

    public void setCatalog(String catalog) {
        this.catalog = catalog;
    }

    public void setSchema(String schema) {
        this.schema = schema;
    }

    public boolean isCascading() {
        return cascading;
    }

    public void setCascading(boolean cascading) {
        this.cascading = cascading;
    }

    public String[] getIgnore() {
        return ignore;
    }

    public void setIgnore(String[] ignore) {
        this.ignore = ignore;
    }

    public String[] getPrefixed() {
        return prefixed;
    }

    public void setPrefixed(String[] prefixed) {
        this.prefixed = prefixed;
    }

    public int getInterval() {
        return interval;
    }

    public void setInterval(int interval) {
        this.interval = interval;
    }

    public Random getRand() {
        if (rand == null) {
            rand = new java.util.Random();
        }
        return rand;
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    public void setVerbose(boolean verbose) {
        this.verbose = verbose;
    }

    public void setDmlWeight(int[] dmlWeight) {
        this.dmlWeight = dmlWeight;
    }

    public void setContinueOnError(boolean continueOnError) {
        this.continueOnError = continueOnError;
    }

    public void setPrint(boolean print) {
        this.print = print;
    }

    public boolean getPrint() {
        return print;
    }

    public int getInsertWeight() {
        return dmlWeight[0];
    }

    public int getUpdateWeight() {
        return dmlWeight[1];
    }

    public int getDeleteWeight() {
        return dmlWeight[2];
    }

    public void setTextColumnExpression(String textColumnExpression) {
        this.textColumnExpression = textColumnExpression;
    }

    public String getTextColumnExpression() {
        return textColumnExpression;
    }
}