package org.jcommons.db.column;
import java.util.*;
import org.apache.commons.lang.StringUtils;
import org.jcommons.io.data.DataProvider;
import org.jcommons.lang.string.NamedString;
import org.jcommons.message.*;
/**
* Data provider that converts plain data using the meta column data from the database itself.
*
* @author Thorsten Goeckeler
*/
public class ColumnDataProvider
implements DataProvider
{
private List<MetaColumn> metaColumns;
private Column[] columns;
private String tableName;
private String[] headers;
private String[] values;
private final Map<String, Integer> indices;
private final Message validations;
private static final String COLUMN_REQUIRED = "Table \"${table}\" requires values for column \"${column}\".";
private static final String COLUMN_MISSING = "Table \"${table}\" has no column \"${column}\".";
/** default constructor */
public ColumnDataProvider() {
metaColumns = null;
columns = null;
tableName = null;
headers = null;
indices = new HashMap<String, Integer>();
values = new String[0];
validations = new Messages();
}
/** {@inheritDoc} */
@Override
public void clear() {
// just remove the current values but not the data structure
Arrays.fill(values, null);
validations.clear();
}
private String error(final String message, final String table, final String column) {
return NamedString.message(message).with("table", table).with("column", column).toString();
}
/** {@inheritDoc} */
@Override
public String[] getHeaders() {
return headers == null ? new String[0] : headers;
}
/**
* Find the meta column by a given name.
*
* @param columnName the case insensitive column name
* @return the respective column or <code>null</code> if there is no such column
*/
public MetaColumn getMetaColumn(final String columnName) {
if (StringUtils.isBlank(columnName)) return null;
for (MetaColumn column : getMetaColumns()) {
if (columnName.equalsIgnoreCase(column.getName())) { return column; }
}
return null;
}
/** @return the currently known database meta data for the table associated with this data provider */
public List<MetaColumn> getMetaColumns() {
if (this.metaColumns == null) return Collections.emptyList();
return this.metaColumns;
}
/** @return the current table name, never <code>null</code> */
public String getTable() {
return StringUtils.defaultIfEmpty(tableName, "unknown");
}
/** {@inheritDoc} */
@Override
public Object getValue(final String column) {
if (column == null) return null;
for (int index = 0; index < getHeaders().length; ++index) {
if (column.equalsIgnoreCase(getHeaders()[index])) { return getValueAt(index); }
}
return null;
}
/** {@inheritDoc} */
@Override
public Object getValueAt(final int index) {
validate();
Column[] columns = getColumns();
if (index >= 0 && index < columns.length && values != null && index < values.length) {
Column column = columns[index];
column.setValue(values[index]);
Object object = column.getObject();
validations.add(column.validate());
return object;
}
return null;
}
/** {@inheritDoc} */
@Override
public Object[] getValues() {
// TODO Auto-generated method stub
return null;
}
/** {@inheritDoc} */
@Override
public void setHeaders(final String[] columns) {
this.headers = columns;
this.columns = null;
validations.clear();
// re-index the headers
indices.clear();
if (columns != null) {
for (int index = 0; index < columns.length; ++index) {
indices.put(this.headers[index], index);
}
this.values = new String[columns.length];
}
}
/**
* Provides the typed column headers from the database to be matched with the given table
*
* @param metaColumns the database column definitions
* @return this to allow chaining
*/
public ColumnDataProvider setMetaColumns(final List<MetaColumn> metaColumns) {
this.metaColumns = metaColumns;
this.columns = null;
validations.clear();
return this;
}
/**
* Define the name of the table for which this data conversion takes place.
*
* @param tableName the name of the table that contains all columns, never <code>null</code>
*/
public void setTable(final String tableName) {
this.tableName = tableName;
validations.clear();
}
/** {@inheritDoc} */
@Override
public void setValue(final String column, final String value) {
Integer index = indices.get(column);
if (index != null) setValueAt(index, value);
}
/** {@inheritDoc} */
@Override
public void setValueAt(final int index, final String value) {
validations.clear();
if (index >= 0 || index < values.length) values[index] = value;
}
/** {@inheritDoc} */
@Override
public void setValues(final List<String> values) {
validations.clear();
Arrays.fill(this.values, null);
if (values != null) {
for (int index = 0; index < Math.min(values.size(), this.values.length); ++index) {
this.values[index] = values.get(index);
}
}
}
/** {@inheritDoc} */
@Override
public void setValues(final Map<String, String> values) {
validations.clear();
Arrays.fill(this.values, null);
for (Map.Entry<String, String> item : values.entrySet()) {
this.setValue(item.getKey(), item.getValue());
}
}
/** {@inheritDoc} */
@Override
public void setValues(final String[] values) {
validations.clear();
if (values == null) {
clear();
} else {
if (this.values.length == values.length) {
this.values = values;
} else {
for (int index = 0; index < this.values.length; ++index) {
this.values[index] = values[index];
}
}
}
}
/**
* Validates if the columns can be converted at all that is if the table columns can be mapped to the data columns.
*
* @return the list of error messages, should be empty to start conversion
*/
public Message validateTable() {
Messages errors = new Messages();
if (getMetaColumns().isEmpty() || getTable() == null || headers == null) return errors;
for (MetaColumn column : getMetaColumns()) {
if (column.isNotNullable() && !existsHeader(column.getName())) {
errors.add(new Fault(error(COLUMN_REQUIRED, getTable(), column.getName())));
}
}
for (String column : getHeaders()) {
if (getMetaColumn(column) == null) {
errors.add(new Fault(error(COLUMN_MISSING, getTable(), column)));
}
}
return errors;
}
/**
* Validates if the data or which data could be converted.
*
* @return the list of error messages, should contain no faults for a successful conversion
*/
public Message validate() {
if (validations.isEmpty()) {
// the data is validated on the fly during conversion, so we need to fill this data only if no validations has
// been performed in the meantime
validations.add(validateTable());
}
// only return the validations for the current moment
return validations;
}
/** @return current conversion columns for the current columns and meta-data, never <code>null</code> */
private Column[] getColumns() {
if (columns == null || columns.length == 0) {
if (getHeaders().length > 0 && !getMetaColumns().isEmpty() && getHeaders().length == getMetaColumns().size()) {
columns = new Column[getHeaders().length];
for (int index = 0; index < getHeaders().length; ++index) {
Column column = new Column();
column.setMeta(getMetaColumn(getHeaders()[index]));
}
} else {
columns = new Column[0];
}
}
return columns;
}
/**
* Checks whether the given column name is a provided in the headers to be loaded.
*
* @param header the column name that should be present in data set we want to load
* @return true if the header is provided, otherwise false
*/
private boolean existsHeader(final String header) {
return indexOfHeader(header) >= 0;
}
/**
* Determines the index of the given column header in the data set we want to load.
*
* @param header the column name that should be present in data set we want to load
* @return the corresponding index in the header array or -1 if the header is unknown
*/
private int indexOfHeader(final String header) {
for (int index = 0; index < getHeaders().length; ++index) {
if (getHeaders()[index].equalsIgnoreCase(header)) {
return index;
}
}
return -1;
}
}