/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cayenne.access.jdbc;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.access.types.ExtendedType;
import org.apache.cayenne.access.types.ExtendedTypeMap;
import org.apache.commons.collections.Transformer;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A builder class that helps to assemble {@link RowDescriptor} instances from various
* types of inputs.
*
* @since 3.0
*/
public class RowDescriptorBuilder {
private static final Logger logger = LoggerFactory.getLogger(RowDescriptorBuilder.class);
private static final Transformer UPPERCASE_TRANSFORMER = new Transformer() {
public Object transform(Object input) {
return input != null ? input.toString().toUpperCase() : null;
}
};
private static final Transformer LOWERCASE_TRANSFORMER = new Transformer() {
public Object transform(Object input) {
return input != null ? input.toString().toLowerCase() : null;
}
};
protected ColumnDescriptor[] columns;
protected ResultSetMetaData resultSetMetadata;
protected Transformer caseTransformer;
protected Map<String, String> typeOverrides;
protected boolean validateDuplicateColumnNames;
/**
* Returns a RowDescriptor built based on the builder internal state.
*/
public RowDescriptor getDescriptor(ExtendedTypeMap typeMap) throws SQLException, IllegalStateException {
ColumnDescriptor[] columnsForRD;
if (this.resultSetMetadata != null) {
// do merge between explicitly-set columns and ResultSetMetadata
// explicitly-set columns take precedence
columnsForRD = mergeResultSetAndPresetColumns();
} else if (this.columns != null) {
// use explicitly-set columns
columnsForRD = this.columns;
} else {
throw new IllegalStateException(
"Can't build RowDescriptor, both 'columns' and 'resultSetMetadata' are null");
}
performTransformAndTypeOverride(columnsForRD);
ExtendedType[] converters = new ExtendedType[columnsForRD.length];
for (int i = 0; i < columnsForRD.length; i++) {
converters[i] = typeMap.getRegisteredType(columnsForRD[i].getJavaClass());
}
return new RowDescriptor(columnsForRD, converters);
}
/**
* @return array of columns for ResultSet with overriding ColumnDescriptors from
* 'columns' Note: column will be overlooked, if column name is empty
*/
protected ColumnDescriptor[] mergeResultSetAndPresetColumns() throws SQLException {
int rsLen = resultSetMetadata.getColumnCount();
if (rsLen == 0) {
throw new CayenneRuntimeException("'ResultSetMetadata' is empty.");
}
int columnLen = (columns != null) ? columns.length : 0;
if (rsLen < columnLen) {
throw new CayenneRuntimeException("'ResultSetMetadata' has less elements then 'columns'.");
} else if (rsLen == columnLen) {
// 'columns' contains ColumnDescriptor for every column
// in resultSetMetadata. This return is for optimization.
return columns;
}
ColumnDescriptor[] rsColumns = new ColumnDescriptor[rsLen];
List<String> duplicates = null;
Set<String> uniqueNames = null;
if(validateDuplicateColumnNames) {
duplicates = new ArrayList<>();
uniqueNames = new HashSet<>();
}
int outputLen = 0;
for (int i = 0; i < rsLen; i++) {
String rowkey = resolveDataRowKeyFromResultSet(i + 1);
// resolve column descriptor from 'columns' or create new
ColumnDescriptor descriptor = getColumnDescriptor(rowkey, columns, i + 1);
// validate uniqueness of names
if(validateDuplicateColumnNames) {
if(!uniqueNames.add(descriptor.getDataRowKey())) {
duplicates.add(descriptor.getDataRowKey());
}
}
rsColumns[outputLen] = descriptor;
outputLen++;
}
if(validateDuplicateColumnNames && !duplicates.isEmpty()) {
logger.warn("Found duplicated columns '" + StringUtils.join(duplicates, "', '") + "' in row descriptor. " +
"This can lead to errors when converting result to persistent objects.");
}
if (outputLen < rsLen) {
// cut ColumnDescriptor array
ColumnDescriptor[] rsColumnsCut = new ColumnDescriptor[outputLen];
System.arraycopy(rsColumns, 0, rsColumnsCut, 0, outputLen);
return rsColumnsCut;
}
return rsColumns;
}
/**
* @return ColumnDescriptor from columnArray, if columnArray contains descriptor for
* this column, or new ColumnDescriptor.
*/
private ColumnDescriptor getColumnDescriptor(
String rowKey,
ColumnDescriptor[] columnArray,
int position) throws SQLException {
int len = (columnArray != null) ? columnArray.length : 0;
// go through columnArray to find ColumnDescriptor for specified column
for (int i = 0; i < len; i++) {
if (columnArray[i] != null) {
String columnRowKey = columnArray[i].getDataRowKey();
// TODO: andrus, 10/14/2009 - 'equalsIgnoreCase' check can result in
// subtle bugs in DBs with case-sensitive column names (or when quotes are
// used to force case sensitivity). Alternatively using 'equals' may miss
// columns in case-insensitive situations.
if (columnRowKey != null && columnRowKey.equalsIgnoreCase(rowKey)) {
return columnArray[i];
}
}
}
// columnArray doesn't contain ColumnDescriptor for specified column
return new ColumnDescriptor(rowKey, resultSetMetadata, position);
}
/**
* Return not empty string with ColumnLabel or ColumnName or "column_" + position for
* for specified (by it's position) column in ResultSetMetaData.
*/
private String resolveDataRowKeyFromResultSet(int position) throws SQLException {
String name = resultSetMetadata.getColumnLabel(position);
if (name == null || name.length() == 0) {
name = resultSetMetadata.getColumnName(position);
if (name == null) {
name = "";
}
}
return name;
}
private void performTransformAndTypeOverride(ColumnDescriptor[] columnArray) {
int len = columnArray.length;
if (caseTransformer != null) {
for (ColumnDescriptor aColumnArray : columnArray) {
aColumnArray.setDataRowKey((String) caseTransformer.transform(aColumnArray.getDataRowKey()));
aColumnArray.setName((String) caseTransformer.transform(aColumnArray.getName()));
}
}
if (typeOverrides != null) {
for (ColumnDescriptor aColumnArray : columnArray) {
String type = typeOverrides.get(aColumnArray.getName());
if (type != null) {
aColumnArray.setJavaClass(type);
}
}
}
}
/**
* Sets an explicit set of columns. Note that the array passed as an argument can
* later be modified by the build to enforce column capitalization policy and columns
* Java types overrides.
*/
public RowDescriptorBuilder setColumns(ColumnDescriptor[] columns) {
this.columns = columns;
return this;
}
public RowDescriptorBuilder setResultSet(ResultSet resultSet) throws SQLException {
this.resultSetMetadata = resultSet.getMetaData();
return this;
}
public RowDescriptorBuilder useLowercaseColumnNames() {
this.caseTransformer = LOWERCASE_TRANSFORMER;
return this;
}
public RowDescriptorBuilder useUppercaseColumnNames() {
this.caseTransformer = UPPERCASE_TRANSFORMER;
return this;
}
public RowDescriptorBuilder overrideColumnType(String columnName, String type) {
if (typeOverrides == null) {
typeOverrides = new HashMap<>();
}
typeOverrides.put(columnName, type);
return this;
}
/**
* Validate and report duplicate names of columns.
* @return this builder
*/
public RowDescriptorBuilder validateDuplicateColumnNames() {
this.validateDuplicateColumnNames = true;
return this;
}
public boolean isOverriden(String columnName) {
return typeOverrides != null && typeOverrides.containsKey(columnName);
}
}