org.reusables.dbunit.autocomplete.AutoCompletionDataset.java Source code

Java tutorial

Introduction

Here is the source code for org.reusables.dbunit.autocomplete.AutoCompletionDataset.java

Source

/*
 * Copyright 2010-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.reusables.dbunit.autocomplete;

import org.reusables.dbunit.autocomplete.AutoCompletionColumn.ColumnType;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.math.RandomUtils;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.DefaultTableIterator;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ITableIterator;
import org.dbunit.dataset.ITableMetaData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Dataset that wraps another dataset, adding automatic column value completion.
 * 
 * <p>
 * The auto completion is done based on a ruleset that is read from an xml file.
 * </p>
 * 
 * <p>
 * Example rules file:
 * <pre>
 * &lt;dataset-rules&gt;
 *    &lt;table name="TBL_ORDER"&gt;
 *       &lt;pk name="ID" min="1000000" max="1999999"/&gt;
 *       &lt;string name="CODE" min="1000000" max="1999999"&gt;code_&lt;/string&gt;
 *       &lt;number name="TOTAL_AMOUNT" min="100" max="999"/&gt;
 *       &lt;fk name="CUSTOMER_ID" min="1000000" max="1999999" reference="TBL_CUSTOMER"/&gt;
 *    &lt;/table&gt;
 * 
 *    more table elements...
 * &lt;/dataset-rules&gt;
 * </pre>
 * </p>
 * 
 * <p>
 * For each table you specify the <b>name</b> attribute, each column also has a <b>name</b> attribute. The type of the column is determained by the element name.
 * The available types are <b>string</b>, <b>number</b>, <b>pk</b> and <b>fk</b>.
 * <p/>
 * 
 * <p>
 * For columns you can specify a range for generating a random value, using a <b>max</b> attribute and optionally a <b>min</b> attribute.
 * If you don't specify a range you need to specify an element value which is then used as static value for the column.
 * e.g.:
 * <pre>
 * &lt;string name="DESCRIPTION"&gt;some static description&lt;/string&gt;
 * </pre>
 * </p>
 * 
 * <p>
 * If you specify both the range and a value, the value is used as prefix for the random number from the range.
 * For number columns the value is ignored when a range is specified.
 * </p>
 * 
 * <p>
 * For foreign key columns you can use the <b>fk</b> type. For these columns you also need the specify the <b>reference</b> attribute.
 * When encountering a dataset record that has a ruleset with a fk a record for the referenced table is then generated as well.
 * When generating a record for a referenced table the <b>pk</b> column for that table is used to find the referenced column by the foreign key.
 * </p>
 * 
 * <p>
 * By specifying the <b>optional</b> attribute in the ruleset you can specify that the column is not mandatory, but you still
 * want to specify it's rules. In your dataset you can force the generation of a value based on the ruleset by giving the column
 * the value "${gen}". e.g.:
 * <pre>
 * &lt;fk name="SOME_OTHER_ID" mix="10000" max="19999" reference="SOME_OTHER_TABLE" optional="true"/&gt;
 * </pre>
 * Dataset:
 * <pre>
 * &lt;SOME_TABLE ID="17237" SOME_ID="${gen}"/&gt;
 * </pre>
 * </p>
 * 
 * <p>
 * Sometimes you might want to specify the foreign key value yourself, but still want to generate a record in the referenced table.
 * You can do this by specifying the value ${gen[:VALUE]}.
 * e.g.:
 * <pre>
 * &lt;SOME_TABLE ID="17237" SOME_ID="${gen:14101}"/&gt;
 * </pre>
 * </p>
 * 
 * <p>
 * This will generate a record with pk value '14101'.
 * </p>
 * 
 * <p>
 * Instead of ${gen[:VALUE]} you can also use your own syntax, by specifying the attributes <b>gen-prefix</b>, <b>gen-postfix</b> and <b>gen-separator</b>
 * on your <b>dataset-rules</b> element. A value for gen-prefix is mandatory, the other two may be empty.
 * </p>
 * 
 * <p>Default is:</p>
 * <pre>
 * &lt;dataset-rules gen-prefix="${gen" gen-postfix="}" gen-separator=":"
 * </pre>
 * 
 * <p>This example allows you to specify your own id's like this <i>$$$[VALUE]</i>.</p>
 * <pre>
 * &lt;dataset-rules gen-prefix="$$$" gen-postfix="" gen-separator=""
 * </pre>
 * 
 * @author marcel
 * @since 1.1
 **/
public final class AutoCompletionDataset implements IDataSet {
    private static final Logger LOG = LoggerFactory.getLogger(AutoCompletionColumn.class);

    private static final String[] EMPTY_NAME_ARRAY = new String[0];
    private static final ITable[] EMPTY_TABLE_ARRAY = new ITable[0];

    private final boolean caseSensitiveTableNames;
    private final List<SimpleTable> tables = new ArrayList<SimpleTable>();
    private final Map<String, SimpleTable> tableMap = new HashMap<String, SimpleTable>();

    /**
     * Constuctor.
     * @param targetDataset The dataset to wrap and auto complete.
     * @param rulesFileUrl The url to read the auto completion rules from.
     */
    public AutoCompletionDataset(final IDataSet targetDataset, final URL rulesFileUrl) {
        init(targetDataset, rulesFileUrl);
        this.caseSensitiveTableNames = targetDataset.isCaseSensitiveTableNames();
    }

    @Override
    public String[] getTableNames() throws DataSetException {
        return this.tableMap.keySet().toArray(EMPTY_NAME_ARRAY);
    }

    @Override
    public ITableMetaData getTableMetaData(final String tableName) throws DataSetException {
        return this.tableMap.get(tableName).getTableMetaData();
    }

    @Override
    public ITable getTable(final String tableName) throws DataSetException {
        return this.tableMap.get(tableName);
    }

    @SuppressWarnings("deprecation") // Have to override this deprecated method. 
    @Override
    public ITable[] getTables() throws DataSetException {
        return getTableArray();
    }

    @Override
    public ITableIterator iterator() throws DataSetException {
        return new DefaultTableIterator(getTableArray());
    }

    @Override
    public ITableIterator reverseIterator() throws DataSetException {
        return new DefaultTableIterator(getTableArray(), true);
    }

    @Override
    public boolean isCaseSensitiveTableNames() {
        return this.caseSensitiveTableNames;
    }

    private void init(final IDataSet targetDataset, final URL rulesFileUrl) {
        Validate.notNull(targetDataset, "No 'targetDataset' supplied.");
        Validate.notNull(rulesFileUrl, "No auto completion rules file URL supplied.");

        final AutoCompletionRules rules = AutoCompletionRules.parseRules(rulesFileUrl);

        try {
            copyDataSet(targetDataset);
            autoComplete(rules, this.tables);
            Collections.sort(this.tables, new ForeignKeyTableComparator(rules));
        } catch (final DataSetException e) {
            throw new DbUnitAutoCompletionException("Initialization error", e);
        }
    }

    private void copyDataSet(final IDataSet targetDataset) throws DataSetException {
        final List<SimpleTable> copiedTables = new ArrayList<SimpleTable>();

        for (final ITableIterator iter = targetDataset.iterator(); iter.next();) {
            copiedTables.add(SimpleTable.copy(iter.getTable()));
        }

        registerTables(copiedTables, false);
    }

    private void autoComplete(final AutoCompletionRules rules, final Collection<SimpleTable> tablesToComplete)
            throws DataSetException {
        final Set<SimpleTable> additionalTables = new HashSet<SimpleTable>();

        for (final SimpleTable table : tablesToComplete) {
            final String tableName = table.getTableMetaData().getTableName();
            LOG.debug("autoComplete :: table = {}", tableName);

            for (final AutoCompletionColumn column : rules.getColumns(tableName)) {
                LOG.debug("autoComplete :: column = {}", column);
                autoCompleteColumn(table, column, rules, additionalTables);
            }
        }

        if (!additionalTables.isEmpty()) {
            registerTables(additionalTables, true);
            autoComplete(rules, additionalTables);
        }
    }

    private void autoCompleteColumn(final SimpleTable table, final AutoCompletionColumn column,
            final AutoCompletionRules rules, final Set<SimpleTable> additionalTables) throws DataSetException {
        final String columnName = column.getName();
        boolean columnExists = table.getTableMetaData().hasColumn(columnName);
        LOG.debug("autoComplete :: columnExists = {}", columnExists);

        for (int row = 0; row < table.getRowCount(); row++) {
            final Object value = !columnExists ? null : table.getValue(row, columnName);
            LOG.debug("autoComplete :: row = {}, value = {}", row, value);

            final boolean noValue = value == null || value.equals(ITable.NO_VALUE);
            final boolean genKeyword = value instanceof String
                    && StringUtils.startsWith((String) value, rules.getGenPrefix());

            if (noValue && !column.isOptional() || genKeyword) {
                if (!columnExists) {
                    table.getTableMetaData().addColumn(columnName);
                    columnExists = true;
                }

                final Object newValue = generateValue(column, value, rules);
                table.setValue(row, columnName, newValue);

                generateReferencedRow(column, newValue, rules, additionalTables);
            }
        }
    }

    private void generateReferencedRow(final AutoCompletionColumn column, final Object newValue,
            final AutoCompletionRules rules, final Set<SimpleTable> additionalTables) throws DataSetException {
        if (!column.getType().equals(ColumnType.FK)) {
            return;
        }

        final String tableName = column.getReference();
        final SimpleTable referencedTable = createTable(tableName, additionalTables);
        final int rowIndex = referencedTable.addRow();
        final AutoCompletionColumn pkColumn = rules.findPkColumn(tableName);

        Validate.notNull(pkColumn, "No PK column found for table: " + tableName);
        referencedTable.getTableMetaData().addColumn(pkColumn.getName());
        referencedTable.setValue(rowIndex, pkColumn.getName(), newValue);
        additionalTables.add(referencedTable);
    }

    private SimpleTable createTable(final String tableName, final Collection<SimpleTable> additionalTables) {
        if (this.tableMap.containsKey(tableName)) {
            return this.tableMap.get(tableName);
        }

        for (final SimpleTable table : additionalTables) {
            if (tableName.equals(table.getTableMetaData().getTableName())) {
                return table;
            }
        }

        return SimpleTable.newInstance(tableName);
    }

    private void registerTables(final Collection<SimpleTable> tablesToRegister, final boolean insertAtStart) {
        for (final SimpleTable table : tablesToRegister) {
            final String tableName = table.getTableMetaData().getTableName();

            if (!this.tableMap.containsKey(tableName)) {
                this.tableMap.put(tableName, table);

                if (insertAtStart) {
                    this.tables.add(0, table);
                } else {
                    this.tables.add(table);
                }
            }
        }
    }

    private ITable[] getTableArray() {
        return this.tables.toArray(EMPTY_TABLE_ARRAY);
    }

    private Object generateValue(final AutoCompletionColumn column, final Object orgValue,
            final AutoCompletionRules rules) {
        if (orgValue instanceof String) {
            final String str = (String) orgValue;
            final String prefix = rules.getGenPrefixPlusSeparator();
            final String postfix = rules.getGenPostfix();

            if (str.length() > prefix.length() + postfix.length() && str.startsWith(prefix)
                    && (StringUtils.isEmpty(postfix) || str.endsWith(postfix))) {
                final String orgValueStr = str;
                return StringUtils.substring(orgValueStr, prefix.length(), orgValueStr.length() - postfix.length());
            }
        }

        final Integer randomValue = getRandomValue(column);
        final String value = StringUtils.defaultString(column.getValue());

        switch (column.getType()) {
        case NUMBER:
            return randomValue != null ? randomValue : value;
        case FK:
        case PK:
        case STRING:
            return String.format("%s%s", value, ObjectUtils.toString(randomValue));
        }

        return ITable.NO_VALUE;
    }

    private Integer getRandomValue(final AutoCompletionColumn column) {
        if (column.getMax() != null) {
            final int min = column.getMin() == null ? 0 : column.getMin().intValue();
            return RandomUtils.nextInt(column.getMax().intValue() - min + 1) + min;
        }
        return null;
    }
}