/**
* AnalyzerBeans
* Copyright (C) 2014 Neopost - Customer Information Management
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.eobjects.analyzer.beans.transform;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import org.eobjects.analyzer.beans.api.Alias;
import org.eobjects.analyzer.beans.api.Close;
import org.eobjects.analyzer.beans.api.ColumnProperty;
import org.eobjects.analyzer.beans.api.Concurrent;
import org.eobjects.analyzer.beans.api.Configured;
import org.eobjects.analyzer.beans.api.Description;
import org.eobjects.analyzer.beans.api.Initialize;
import org.eobjects.analyzer.beans.api.MappedProperty;
import org.eobjects.analyzer.beans.api.OutputColumns;
import org.eobjects.analyzer.beans.api.OutputRowCollector;
import org.eobjects.analyzer.beans.api.Provided;
import org.eobjects.analyzer.beans.api.SchemaProperty;
import org.eobjects.analyzer.beans.api.TableProperty;
import org.eobjects.analyzer.beans.api.Transformer;
import org.eobjects.analyzer.beans.api.TransformerBean;
import org.eobjects.analyzer.beans.api.Validate;
import org.eobjects.analyzer.connection.Datastore;
import org.eobjects.analyzer.connection.DatastoreConnection;
import org.eobjects.analyzer.data.InputColumn;
import org.eobjects.analyzer.data.InputRow;
import org.eobjects.analyzer.util.CollectionUtils2;
import org.eobjects.analyzer.util.HasLabelAdvice;
import org.apache.metamodel.data.DataSet;
import org.apache.metamodel.query.CompiledQuery;
import org.apache.metamodel.query.OperatorType;
import org.apache.metamodel.query.Query;
import org.apache.metamodel.query.QueryParameter;
import org.apache.metamodel.schema.Column;
import org.apache.metamodel.schema.Table;
import org.apache.metamodel.util.HasName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
/**
* A transformer that can do a lookup (like a left join) based on a set of
* columns in any datastore.
*/
@TransformerBean("Table lookup")
@Alias("Datastore lookup")
@Description("Perform a lookup based on a table in any of your registered datastore (like a LEFT join).")
@Concurrent(true)
public class TableLookupTransformer implements Transformer<Object>, HasLabelAdvice {
private static final Logger logger = LoggerFactory.getLogger(TableLookupTransformer.class);
private static final String PROPERTY_NAME_DATASTORE = "Datastore";
private static final String PROPERTY_NAME_SCHEMA_NAME = "Schema name";
private static final String PROPERTY_NAME_TABLE_NAME = "Table name";
public static enum JoinSemantic implements HasName {
@Alias("LEFT")
LEFT_JOIN_MAX_ONE("Left join (max 1 record)"),
@Alias("INNER")
INNER_JOIN("Inner join"),
@Alias("INNER_MIN_ONE")
LEFT_JOIN("Left join");
private final String _name;
private JoinSemantic(String name) {
_name = name;
}
@Override
public String getName() {
return _name;
}
public boolean isCacheable() {
// inner joined result sets are not cached since their size exceeds
// the cache capabilities.
return this == LEFT_JOIN_MAX_ONE;
}
}
@Inject
@Configured(value = PROPERTY_NAME_DATASTORE)
Datastore datastore;
@Inject
@Configured(required = false)
InputColumn<?>[] conditionValues;
@Inject
@Configured(required = false)
@ColumnProperty
@MappedProperty(PROPERTY_NAME_TABLE_NAME)
String[] conditionColumns;
@Inject
@Configured
@ColumnProperty
@MappedProperty(PROPERTY_NAME_TABLE_NAME)
String[] outputColumns;
@Inject
@Configured(value = PROPERTY_NAME_SCHEMA_NAME)
@Alias("Schema")
@SchemaProperty
@MappedProperty(PROPERTY_NAME_DATASTORE)
String schemaName;
@Inject
@Configured(value = PROPERTY_NAME_TABLE_NAME)
@Alias("Table")
@TableProperty
@MappedProperty(PROPERTY_NAME_SCHEMA_NAME)
String tableName;
@Inject
@Configured
@Description("Use a client-side cache to avoid looking up multiple times with same inputs.")
boolean cacheLookups = true;
@Inject
@Configured
@Description("Which kind of semantic to apply to the lookup, compared to a SQL JOIN.")
JoinSemantic joinSemantic = JoinSemantic.LEFT_JOIN_MAX_ONE;
@Inject
@Provided
OutputRowCollector outputRowCollector;
private final Cache<List<Object>, Object[]> cache = CollectionUtils2.<List<Object>, Object[]> createCache(10000,
5 * 60);
private Column[] queryOutputColumns;
private Column[] queryConditionColumns;
private DatastoreConnection datastoreConnection;
private CompiledQuery lookupQuery;
/**
* Default constructor
*/
public TableLookupTransformer() {
}
/**
* Constructor for direct usage within e.g. other components where we always
* expect to do LEFT JOIN (max one record) semantic lookups.
*
* @param datastore
* @param schemaName
* @param tableName
* @param conditionColumns
* @param conditionValues
* @param outputColumns
* @param joinSemantic
* @param cacheLookups
*/
public TableLookupTransformer(Datastore datastore, String schemaName, String tableName, String[] conditionColumns,
InputColumn<?>[] conditionValues, String[] outputColumns, boolean cacheLookups) {
this.datastore = datastore;
this.schemaName = schemaName;
this.tableName = tableName;
this.conditionColumns = conditionColumns;
this.conditionValues = conditionValues;
this.cacheLookups = cacheLookups;
this.outputColumns = outputColumns;
this.joinSemantic = JoinSemantic.LEFT_JOIN_MAX_ONE;
}
@Override
public String getSuggestedLabel() {
if (tableName == null) {
return null;
}
return "Lookup: " + tableName;
}
private void resetCachedColumns() {
queryOutputColumns = null;
queryConditionColumns = null;
}
private Column[] getQueryConditionColumns() {
if (queryConditionColumns == null) {
if (isCarthesianProductMode()) {
queryConditionColumns = new Column[0];
} else {
try (final DatastoreConnection con = datastore.openConnection()) {
queryConditionColumns = con.getSchemaNavigator().convertToColumns(schemaName, tableName,
conditionColumns);
}
}
}
return queryConditionColumns;
}
/**
* Gets the output columns of the lookup query
*
* @param checkNames
* whether to check/validate/adjust the names of these columns
* @return
*/
private Column[] getQueryOutputColumns(boolean checkNames) {
if (queryOutputColumns == null) {
try (final DatastoreConnection con = datastore.openConnection()) {
queryOutputColumns = con.getSchemaNavigator().convertToColumns(schemaName, tableName, outputColumns);
}
} else if (checkNames) {
if (!isQueryOutputColumnsUpdated()) {
queryOutputColumns = null;
return getQueryOutputColumns(false);
}
}
return queryOutputColumns;
}
/**
* Checks the validity of the current (cached) output columns array.
*
* @return true if the current columns are valid
*/
private boolean isQueryOutputColumnsUpdated() {
if (queryOutputColumns.length != outputColumns.length) {
return false;
}
for (int i = 0; i < queryOutputColumns.length; i++) {
Column outputColumn = queryOutputColumns[i];
String expectedName = outputColumns[i];
if (!expectedName.equals(outputColumn.getName())) {
return false;
}
if (tableName != null && !tableName.equals(outputColumn.getTable().getName())) {
return false;
}
}
return true;
}
@Initialize
public void init() {
datastoreConnection = datastore.openConnection();
resetCachedColumns();
cache.invalidateAll();
compileLookupQuery();
}
private void compileLookupQuery() {
try {
final Column[] queryOutputColumns = getQueryOutputColumns(false);
final Column queryOutputColumn = queryOutputColumns[0];
final Table table = queryOutputColumn.getTable();
Query query = new Query().from(table).select(queryOutputColumns);
if (!isCarthesianProductMode()) {
final Column[] queryConditionColumns = getQueryConditionColumns();
for (int i = 0; i < queryConditionColumns.length; i++) {
query = query.where(queryConditionColumns[i], OperatorType.EQUALS_TO, new QueryParameter());
}
}
if (joinSemantic == JoinSemantic.LEFT_JOIN_MAX_ONE) {
query = query.setMaxRows(1);
}
lookupQuery = datastoreConnection.getDataContext().compileQuery(query);
} catch (RuntimeException e) {
logger.error("Error occurred while compiling lookup query", e);
throw e;
}
}
private boolean isCarthesianProductMode() {
return (conditionColumns == null || conditionColumns.length == 0)
&& (conditionValues == null || conditionValues.length == 0);
}
@Validate
public void validate() {
if (isCarthesianProductMode()) {
// carthesian product mode
return;
}
final Column[] queryConditionColumns = getQueryConditionColumns();
final List<String> columnsNotFound = new ArrayList<String>();
for (int i = 0; i < queryConditionColumns.length; i++) {
if (queryConditionColumns[i] == null) {
columnsNotFound.add(conditionColumns[i]);
}
}
if (!columnsNotFound.isEmpty()) {
throw new IllegalArgumentException("Could not find column(s): " + columnsNotFound);
}
}
@Override
public OutputColumns getOutputColumns() {
Column[] queryOutputColumns = getQueryOutputColumns(true);
String[] names = new String[queryOutputColumns.length];
Class<?>[] types = new Class[queryOutputColumns.length];
for (int i = 0; i < queryOutputColumns.length; i++) {
Column column = queryOutputColumns[i];
if (column == null) {
throw new IllegalArgumentException("Could not find column: " + outputColumns[i]);
}
names[i] = column.getName() + " (lookup)";
types[i] = column.getType().getJavaEquivalentClass();
}
return new OutputColumns(names, types);
}
@Override
public Object[] transform(InputRow inputRow) {
final List<Object> queryInput;
if (isCarthesianProductMode()) {
queryInput = Collections.emptyList();
} else {
queryInput = new ArrayList<Object>(conditionValues.length);
for (InputColumn<?> inputColumn : conditionValues) {
Object value = inputRow.getValue(inputColumn);
queryInput.add(value);
}
}
logger.info("Looking up based on condition values: {}", queryInput);
Object[] result;
if (cacheLookups && joinSemantic.isCacheable()) {
result = cache.getIfPresent(queryInput);
if (result == null) {
result = performQuery(queryInput);
cache.put(queryInput, result);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Returning cached lookup result: {}", Arrays.toString(result));
}
}
} else {
result = performQuery(queryInput);
}
return result;
}
private Object[] performQuery(List<Object> queryInput) {
try {
final Column[] queryConditionColumns = getQueryConditionColumns();
final Object[] parameterValues = new Object[queryConditionColumns.length];
for (int i = 0; i < queryConditionColumns.length; i++) {
parameterValues[i] = queryInput.get(i);
}
try (final DataSet dataSet = datastoreConnection.getDataContext()
.executeQuery(lookupQuery, parameterValues)) {
return handleDataSet(dataSet);
}
} catch (RuntimeException e) {
logger.error("Error occurred while looking up based on conditions: " + queryInput, e);
throw e;
}
}
private Object[] handleDataSet(DataSet dataSet) {
if (!dataSet.next()) {
logger.info("Result of lookup: None!");
switch (joinSemantic) {
case LEFT_JOIN_MAX_ONE:
case LEFT_JOIN:
return new Object[outputColumns.length];
default:
return null;
}
}
do {
final Object[] result = dataSet.getRow().getValues();
if (logger.isInfoEnabled()) {
logger.info("Result of lookup: " + Arrays.toString(result));
}
switch (joinSemantic) {
case LEFT_JOIN_MAX_ONE:
return result;
default:
outputRowCollector.putValues(result);
}
} while (dataSet.next());
return null;
}
@Close
public void close() {
if (lookupQuery != null) {
lookupQuery.close();
lookupQuery = null;
}
if (datastore != null) {
datastoreConnection.close();
datastoreConnection = null;
}
cache.invalidateAll();
queryOutputColumns = null;
queryConditionColumns = null;
}
}