Java tutorial
/* * 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> * <dataset-rules> * <table name="TBL_ORDER"> * <pk name="ID" min="1000000" max="1999999"/> * <string name="CODE" min="1000000" max="1999999">code_</string> * <number name="TOTAL_AMOUNT" min="100" max="999"/> * <fk name="CUSTOMER_ID" min="1000000" max="1999999" reference="TBL_CUSTOMER"/> * </table> * * more table elements... * </dataset-rules> * </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> * <string name="DESCRIPTION">some static description</string> * </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> * <fk name="SOME_OTHER_ID" mix="10000" max="19999" reference="SOME_OTHER_TABLE" optional="true"/> * </pre> * Dataset: * <pre> * <SOME_TABLE ID="17237" SOME_ID="${gen}"/> * </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> * <SOME_TABLE ID="17237" SOME_ID="${gen:14101}"/> * </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> * <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> * <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; } }