blob: a9a80f1ccc261f84efb3d2375b9a5b54e985e8c4 [file] [log] [blame]
/*
* 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.openjpa.jdbc.meta;
import java.io.Serializable;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.identifier.DBIdentifier.DBIdentifierType;
import org.apache.openjpa.jdbc.identifier.Normalizer;
import org.apache.openjpa.jdbc.identifier.QualifiedDBIdentifier;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ColumnIO;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Index;
import org.apache.openjpa.jdbc.schema.PrimaryKey;
import org.apache.openjpa.jdbc.schema.Schema;
import org.apache.openjpa.jdbc.schema.SchemaGroup;
import org.apache.openjpa.jdbc.schema.Schemas;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.schema.Unique;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataContext;
import org.apache.openjpa.util.MetaDataException;
/**
* Base class storing raw mapping information; defines utility methods for
* converting raw mapping information to full mapping to the schema.
*
* @author Abe White
*/
public abstract class MappingInfo implements Serializable {
private static final long serialVersionUID = 1L;
public static final int JOIN_NONE = 0;
public static final int JOIN_FORWARD = 1;
public static final int JOIN_INVERSE = 2;
private static final Object NULL = new Object();
private static final Localizer _loc = Localizer.forPackage
(MappingInfo.class);
private String _strategy = null;
private List<Column> _cols = null;
private Index _idx = null;
private Unique _unq = null;
private ForeignKey _fk = null;
private boolean _canIdx = true;
private boolean _canUnq = true;
private boolean _canFK = true;
private boolean _implicitRelation = false;
private int _join = JOIN_NONE;
private ColumnIO _io = null;
/**
* Mapping strategy name.
*/
public String getStrategy() {
return _strategy;
}
/**
* Mapping strategy name.
*/
public void setStrategy(String strategy) {
_strategy = strategy;
}
/**
* Raw column data.
*/
public List<Column> getColumns() {
if (_cols == null) {
return Collections.emptyList();
}
return _cols;
}
/**
* Gets the columns whose table name matches the given table name.
* @deprecated
*/
@Deprecated
public List<Column> getColumns(String tableName) {
return getColumns(DBIdentifier.newTable(tableName));
}
/**
* Gets the columns whose table name matches the given table name.
*/
public List<Column> getColumns(DBIdentifier tableName) {
if (_cols == null)
return Collections.emptyList();
List<Column> result = new ArrayList<>();
for (Column col : _cols) {
if (DBIdentifier.equal(col.getTableIdentifier(),
tableName))
result.add(col);
}
return result;
}
/**
* Raw column data.
*/
public void setColumns(List<Column> cols) {
_cols = cols;
}
/**
* Raw index.
*/
public Index getIndex() {
return _idx;
}
/**
* Raw index.
*/
public void setIndex(Index idx) {
_idx = idx;
}
/**
* The user can mark columns as explicitly non-indexable.
*/
public boolean canIndex() {
return _canIdx;
}
/**
* The user can mark columns as explicitly non-indexable.
*/
public void setCanIndex(boolean indexable) {
_canIdx = indexable;
}
/**
* Affirms if this instance represents an implicit relation. For example, a
* relation expressed as the value of primary key of the related class and
* not as object reference.
*
* @since 1.3.0
*/
public boolean isImplicitRelation() {
return _implicitRelation;
}
/**
* Sets a marker to imply a logical relation that can not have any physical
* manifest in the database. For example, a relation expressed as the value
* of primary key of the related class and not as object reference.
* Populated from @ForeignKey(implicit=true) annotation.
* The mutator can only transit from false to true but not vice versa.
*
* @since 1.3.0
*/
public void setImplicitRelation(boolean flag) {
_implicitRelation |= flag;
}
/**
* Raw foreign key information.
*/
public ForeignKey getForeignKey() {
return _fk;
}
/**
* Raw foreign key information.
*/
public void setForeignKey(ForeignKey fk) {
_fk = fk;
if (fk != null && _join == JOIN_NONE)
_join = JOIN_FORWARD;
}
/**
* The user can mark columns as explicitly not having a foreign key.
*/
public boolean canForeignKey() {
return _canFK;
}
/**
* The user can mark columns as explicitly not having a foreign key.
*/
public void setCanForeignKey(boolean fkable) {
_canFK = fkable;
}
/**
* Raw unique constraint information.
*/
public Unique getUnique() {
return _unq;
}
/**
* Raw unique constraint information.
*/
public void setUnique(Unique unq) {
_unq = unq;
}
/**
* The user can mark columns as explicitly not having a unique constraint.
*/
public boolean canUnique() {
return _canUnq;
}
/**
* The user can mark columns as explicitly not having a unique constraint.
*/
public void setCanUnique(boolean uniquable) {
_canUnq = uniquable;
}
/**
* I/O for the columns created by the last call to {@link #createColumns},
* or for the foreign key created by the last call to
* {@link #createForeignKey}. This is also expected to be set correctly
* prior to calls to {@link #syncColumns} and {@link #syncForeignKey}.
*/
public ColumnIO getColumnIO() {
return _io;
}
/**
* I/O for the columns created by the last call to {@link #createColumns},
* or for the foreign key created by the last call to
* {@link #createForeignKey}. This is also expected to be set correctly
* prior to calls to {@link #syncColumns} and {@link #syncForeignKey}.
*/
public void setColumnIO(ColumnIO io) {
_io = io;
}
/**
* Direction of the join that the columns of this mapping info form. This
* is usually automatically set by {@link #createForeignKey}. This flag
* is also expected to be set correctly prior to calls to
* {@link #syncForeignKey} if the join is inversed.
*/
public int getJoinDirection() {
return _join;
}
/**
* Direction of the join that the columns of this mapping info form. This
* is usually automatically set by {@link #createForeignKey}. This flag
* is also expected to be set correctly prior to calls to
* {@link #syncForeignKey} if the join is inversed.
*/
public void setJoinDirection(int join) {
_join = join;
}
/**
* Clear all mapping information.
*/
public void clear() {
clear(true);
}
/**
* Clear mapping information.
*
* @param canFlags whether to clear information about whether we
* can place indexed, foreign keys, etc on this mapping
*/
protected void clear(boolean canFlags) {
_strategy = null;
_cols = null;
_io = null;
_idx = null;
_unq = null;
_fk = null;
_join = JOIN_NONE;
if (canFlags) {
_canIdx = true;
_canFK = true;
_canUnq = true;
}
}
/**
* Copy missing info from the instance to this one.
*/
public void copy(MappingInfo info) {
if (_strategy == null)
_strategy = info.getStrategy();
if (_canIdx && _idx == null) {
if (info.getIndex() != null)
_idx = info.getIndex();
else
_canIdx = info.canIndex();
}
if (_canUnq && _unq == null) {
if (info.getUnique() != null)
_unq = info.getUnique();
else
_canUnq = info.canUnique();
}
if (_canFK && _fk == null) {
if (info.getForeignKey() != null)
_fk = info.getForeignKey();
else
_canFK = info.canForeignKey();
}
_implicitRelation = info.isImplicitRelation();
List<Column> cols = getColumns();
List<Column> icols = info.getColumns();
if (!icols.isEmpty() && (cols.isEmpty()
|| cols.size() == icols.size())) {
if (cols.isEmpty())
cols = new ArrayList<>(icols.size());
for (int i = 0; i < icols.size(); i++) {
if (cols.size() == i)
cols.add(new Column());
cols.get(i).copy(icols.get(i));
}
setColumns(cols);
}
}
/**
* Return true if this info has columns, foreign key information, index
* information, etc.
*/
public boolean hasSchemaComponents() {
return (_cols != null && !_cols.isEmpty())
|| _idx != null
|| _unq != null
|| _fk != null
|| !_canIdx
|| !_canFK
|| !_canUnq;
}
/**
* Assert that the user did not supply any columns, index, unique
* constraint, or foreign key for this mapping.
*/
public void assertNoSchemaComponents(MetaDataContext context, boolean die) {
if (_cols == null || _cols.isEmpty()) {
assertNoIndex(context, die);
assertNoUnique(context, die);
assertNoForeignKey(context, die);
return;
}
Message msg = _loc.get("unexpected-cols", context);
if (die)
throw new MetaDataException(msg);
context.getRepository().getLog().warn(msg);
}
/**
* Assert that this info has the given strategy or no strategy.
*/
public void assertStrategy(MetaDataContext context, Object contextStrat,
Object expected, boolean die) {
if (contextStrat == expected)
return;
String strat;
if (contextStrat == null) {
if (_strategy == null)
return;
if (_strategy.equals(expected.getClass().getName()))
return;
if (expected instanceof Strategy
&& _strategy.equals(((Strategy) expected).getAlias()))
return;
strat = _strategy;
} else if (contextStrat instanceof Strategy)
strat = ((Strategy) contextStrat).getAlias();
else
strat = contextStrat.getClass().getName();
Message msg = _loc.get("unexpected-strategy", context, expected,
strat);
if (die)
throw new MetaDataException(msg);
context.getRepository().getLog().warn(msg);
}
/**
* Assert that the user did not try to place an index on this mapping.
*/
public void assertNoIndex(MetaDataContext context, boolean die) {
if (_idx == null)
return;
Message msg = _loc.get("unexpected-index", context);
if (die)
throw new MetaDataException(msg);
context.getRepository().getLog().warn(msg);
}
/**
* Assert that the user did not try to place a unique constraint on this
* mapping.
*/
public void assertNoUnique(MetaDataContext context, boolean die) {
if (_unq == null)
return;
Message msg = _loc.get("unexpected-unique", context);
if (die)
throw new MetaDataException(msg);
context.getRepository().getLog().warn(msg);
}
/**
* Assert that the user did not try to place a foreign key on this mapping
* or placed an implicit foreign key.
*/
public void assertNoForeignKey(MetaDataContext context, boolean die) {
if (_fk == null || isImplicitRelation())
return;
Message msg = _loc.get("unexpected-fk", context);
if (die)
throw new MetaDataException(msg);
context.getRepository().getLog().warn(msg);
}
/**
* Assert that the user did not try to join.
*/
public void assertNoJoin(MetaDataContext context, boolean die) {
boolean join = false;
if (_cols != null) {
Column col;
for (int i = 0; !join && i < _cols.size(); i++) {
col = _cols.get(i);
if (!DBIdentifier.isNull(col.getTargetIdentifier()))
join = true;
}
}
if (!join)
return;
Message msg = _loc.get("unexpected-join", context);
if (die)
throw new MetaDataException(msg);
context.getRepository().getLog().warn(msg);
}
/**
* Find or generate a table for a mapping.
*
* @param context the mapping that uses the table
* @param def default table name provider
* @param schemaName default schema if known, or null
* @param given given table name
* @param adapt whether we can alter the schema or mappings
* @deprecated
*/
@Deprecated
public Table createTable(MetaDataContext context, TableDefaults def,
String schemaName, String given, boolean adapt) {
return createTable(context, def, DBIdentifier.newSchema(schemaName),
DBIdentifier.newTable(given), adapt);
}
public Table createTable(MetaDataContext context, TableDefaults def,
DBIdentifier schemaName, DBIdentifier given, boolean adapt) {
MappingRepository repos = (MappingRepository) context.getRepository();
if (DBIdentifier.isNull(given) && (def == null || (!adapt
&& !repos.getMappingDefaults().defaultMissingInfo())))
throw new MetaDataException(_loc.get("no-table", context));
if (DBIdentifier.isNull(schemaName)) {
//Check the configuration first for a set Schema to use
schemaName = Schemas.getNewTableSchemaIdentifier((JDBCConfiguration)
repos.getConfiguration());
// If the schemaName is still NULL type then check for a system default schema name
// and if available use it.
if (schemaName != null && (schemaName.getType() == DBIdentifierType.NULL)) {
String name = repos.getMetaDataFactory().getDefaults().getDefaultSchema();
schemaName = (name != null ? DBIdentifier.newSchema(name) : schemaName);
}
}
// if no given and adapting or defaulting missing info, use template
SchemaGroup group = repos.getSchemaGroup();
Schema schema = null;
if (DBIdentifier.isNull(given)) {
schema = group.getSchema(schemaName);
if (schema == null)
schema = group.addSchema(schemaName);
given = def.getIdentifier(schema);
}
QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(given);
if (DBIdentifier.isNull(path.getSchemaName())) {
if (!DBIdentifier.isNull(schemaName)) {
path.setSchemaName(schemaName);
}
} else {
schemaName = path.getSchemaName();
schema = null;
}
// look for named table using full name and findTable, which allows
// the dynamic schema factory to create the table if needed
Table table = group.findTable(path);
if (table != null)
return table;
if (!adapt)
throw new MetaDataException(_loc.get("bad-table", given, context));
// named table doesn't exist; create it
if (schema == null) {
schema = group.getSchema(schemaName);
if (schema == null)
schema = group.addSchema(schemaName);
}
table = schema.getTable(given);
if (table == null)
table = schema.addTable(given);
return table;
}
/**
* Retrieve/create columns on the given table by merging the given
* template information with any user-provided information.
*
* @param context the mapping we're retrieving columns for
* @param prefix localized error message key prefix
* @param tmplates template columns
* @param table the table for the columns
* @param adapt whether we can modify the existing mapping or schema
*/
protected Column[] createColumns(MetaDataContext context, String prefix,
Column[] tmplates, Table table, boolean adapt) {
assertTable(context, table);
if (prefix == null)
prefix = "generic";
// the user has to give the right number of expected columns for this
// mapping, or none at all if we're adapting. can't just given one of
// n columns because we don't know which of the n columns the info
// applies to
List<Column> given = getColumns();
if (context instanceof FieldMapping && ((FieldMapping)context).hasMapsIdCols())
given = ((FieldMapping)context).getValueInfo().getMapsIdColumns();
boolean fill = ((MappingRepository) context.getRepository()).
getMappingDefaults().defaultMissingInfo();
if ((!given.isEmpty() || (!adapt && !fill))
&& given.size() != tmplates.length) {
// also consider when this info has columns from multiple tables
given = getColumns(table.getIdentifier());
if ((!adapt && !fill) && given.size() != tmplates.length) {
// try default table
given = getColumns("");
if ((!adapt && !fill) && given.size() != tmplates.length) {
throw new MetaDataException(_loc.get(prefix + "-num-cols",
context, String.valueOf(tmplates.length),
String.valueOf(given.size())));
}
}
}
Column[] cols = new Column[tmplates.length];
_io = null;
Column col;
for (int i = 0; i < tmplates.length; i++) {
col = (given.isEmpty()) ? null : (Column) given.get(i);
cols[i] = mergeColumn(context, prefix, tmplates[i], true, col,
table, adapt, fill);
setIOFromColumnFlags(col, i);
}
return cols;
}
/**
* Set the proper internal column I/O metadata for the given column's flags.
*/
private void setIOFromColumnFlags(Column col, int i) {
if (col == null || (!col.getFlag(Column.FLAG_UNINSERTABLE)
&& !col.getFlag(Column.FLAG_UNUPDATABLE)
&& !col.isNotNull()))
return;
if (_io == null)
_io = new ColumnIO();
_io.setInsertable(i, !col.getFlag(Column.FLAG_UNINSERTABLE));
_io.setUpdatable(i, !col.getFlag(Column.FLAG_UNUPDATABLE));
_io.setNullInsertable(i, !col.isNotNull());
_io.setNullUpdatable(i, !col.isNotNull());
}
/**
* Assert that the given table is non-null.
*/
private static void assertTable(MetaDataContext context, Table table) {
if (table == null)
throw new MetaDataException(_loc.get("unmapped", context));
}
/**
* Merge the given columns if possible.
*
* @param context the mapping we're retrieving columns for
* @param prefix localized error message key prefix
* @param tmplate template for expected column information
* @param compat whether the existing column type must be compatible
* with the type of the template column
* @param given the given column information from mapping info
* @param table the table for the columns
* @param adapt whether we can modify the existing mapping or schema
* @param fill whether to default missing column information
*/
protected static Column mergeColumn(MetaDataContext context, String prefix,
Column tmplate, boolean compat, Column given, Table table,
boolean adapt, boolean fill) {
assertTable(context, table);
// if not adapting must provide column name at a minimum
DBIdentifier colName = (given == null) ? DBIdentifier.NULL : given.getIdentifier();
if (DBIdentifier.isNull(colName) && !adapt && !fill)
throw new MetaDataException(_loc.get(prefix + "-no-col-name",
context));
MappingRepository repos = (MappingRepository) context.getRepository();
DBDictionary dict = repos.getDBDictionary();
// determine the column name based on given info, or template if none;
// also make sure that if the user gave a column name, he didn't try
// to put the column in an unexpected table
if (DBIdentifier.isNull(colName))
colName = tmplate.getIdentifier();
QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(colName);
if (path.isUnqualifiedColumn()) {
colName = path.getIdentifier();
} else if (!DBIdentifier.isNull(path.getObjectTableName())) {
findTable(context, path.getObjectTableName(), table,
null, null);
colName = path.getUnqualifiedName();
}
// find existing column
Column col = table.getColumn(colName);
if (col == null && !adapt) {
//
// See if column name has already been validated in a dynamic table.
// If so then want to use that validated column name instead. This
// should seldom if ever occur as long as the database dictionaries
// are kept up-to-date.
//
if ((colName.getName().length() > dict.maxColumnNameLength) ||
dict.getInvalidColumnWordSet().contains(DBIdentifier.toUpper(colName).getName()) &&
!(table.getClass().getName().contains("DynamicTable"))) {
colName=dict.getValidColumnName(colName, new Table());
col = table.getColumn(colName);
if (col == null && !adapt) {
throw new MetaDataException(_loc.
get(prefix + "-bad-col-name", context, colName, table));
}
}
else {
throw new MetaDataException(_loc.
get(prefix + "-bad-col-name", context, colName, table));
}
}
// use information from template column by default, allowing any
// user-given specifics to override it
int type = tmplate.getType();
int size = tmplate.getSize();
if (type == Types.OTHER) {
int precis = 0;
int scale = 0;
if(given != null) {
precis = given.getSize();
scale = given.getDecimalDigits();
}
type =
dict.getJDBCType(tmplate.getJavaType(), size == -1, precis,
scale, tmplate.isXML());
}
boolean ttype = true;
int otype = type;
String typeName = tmplate.getTypeName();
Boolean notNull = null;
if (tmplate.isNotNullExplicit())
notNull = (tmplate.isNotNull()) ? Boolean.TRUE : Boolean.FALSE;
int decimals = tmplate.getDecimalDigits();
String defStr = tmplate.getDefaultString();
boolean autoAssign = tmplate.isAutoAssigned();
boolean relationId = tmplate.isRelationId();
boolean implicitRelation = tmplate.isImplicitRelation();
String targetField = tmplate.getTargetField();
if (given != null) {
// use given type if provided, but warn if it isn't compatible with
// the expected column type
if (given.getType() != Types.OTHER) {
ttype = false;
if (compat && !given.isCompatible(type, typeName, size,
decimals)) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-incompat-col",
context, colName, Schemas.getJDBCName(type)));
}
otype = given.getType();
type = dict.getPreferredType(otype);
}
typeName = given.getTypeName();
if (given.getSize() > 0)
size = given.getSize();
decimals = given.getDecimalDigits();
// leave this info as the template defaults unless the user
// explicitly turns it on in the given column
if (given.isNotNullExplicit())
notNull = (given.isNotNull()) ? Boolean.TRUE : Boolean.FALSE;
if (given.getDefaultString() != null)
defStr = given.getDefaultString();
if (given.isAutoAssigned())
autoAssign = true;
if (given.isRelationId())
relationId = true;
if (given.isImplicitRelation())
implicitRelation = true;
}
// default char column size if original type is char (test original
// type rather than final type because orig might be clob, translated
// to an unsized varchar, which is supported by some dbs)
if (size == 0 && (otype == Types.VARCHAR || otype == Types.CHAR))
size = dict.characterColumnSize;
// create column, or make sure existing column matches expected type
if (col == null) {
col = table.addColumn(colName);
col.setType(type);
} else if ((compat || !ttype) && !col.isCompatible(type, typeName,
size, decimals)) {
// if existing column isn't compatible with desired type, die if
// can't adapt, else warn and change the existing column type
Message msg = _loc.get(prefix + "-bad-col", context,
Schemas.getJDBCName(type), col.getDescription());
if (!adapt && !dict.disableSchemaFactoryColumnTypeErrors)
throw new MetaDataException(msg);
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(msg);
col.setType(type);
} else if (given != null && given.getType() != Types.OTHER) {
// as long as types are compatible, set column to expected type
col.setType(type);
}
// always set the java type and autoassign to expected values, even on
// an existing column, since we don't get this from the DB
if (compat)
col.setJavaType(tmplate.getJavaType());
else if (col.getJavaType() == JavaTypes.OBJECT) {
if (given != null && given.getJavaType() != JavaTypes.OBJECT)
col.setJavaType(given.getJavaType());
else
col.setJavaType(JavaTypes.getTypeCode
(Schemas.getJavaType(col.getType(), col.getSize(),
col.getDecimalDigits())));
}
col.setAutoAssigned(autoAssign);
col.setRelationId(relationId);
col.setImplicitRelation(implicitRelation);
col.setTargetField(targetField);
// we need this for runtime, and the dynamic schema factory might
// not know it, so set it even if not adapting
if (defStr != null)
col.setDefaultString(defStr);
if (notNull != null)
col.setNotNull(notNull);
// add other details if adapting
if (adapt) {
if (typeName != null)
col.setTypeName(typeName);
if (size != 0)
col.setSize(size);
if (decimals != 0)
col.setDecimalDigits(decimals);
}
if (tmplate.hasComment())
col.setComment(tmplate.getComment());
if (tmplate.isXML())
col.setXML(tmplate.isXML());
return col;
}
/**
* Find the table named by a column or target.
*
* @param context context for error messages, etc.
* @param name the table name, possibly including schema
* @param expected the expected table; may be null
* @param inverse the possible inverse table; may be null
* @param rel if we're finding the target table of a join, the
* joined-to type; allows us to also look in its superclass tables
*/
private static Table findTable(MetaDataContext context, DBIdentifier name,
Table expected, Table inverse, ClassMapping rel) {
// is this the expected table?
if (expected == null && rel != null)
expected = rel.getTable();
if (expected != null && isTableName(name, expected))
return expected;
// check for inverse
if (inverse != null && isTableName(name, inverse))
return inverse;
// superclass table?
if (rel != null)
rel = rel.getJoinablePCSuperclassMapping();
while (rel != null) {
if (isTableName(name, rel.getTable()))
return rel.getTable();
rel = rel.getJoinablePCSuperclassMapping();
}
// none of the possible tables
throw new MetaDataException(_loc.get("col-wrong-table", context,
expected, name.getName()));
}
/**
* Return whether the given name matches the given table.
*/
private static boolean isTableName(DBIdentifier name, Table table) {
return DBIdentifier.equal(name, table.getIdentifier())
|| DBIdentifier.equal(name, table.getFullIdentifier());
}
/**
* Retrieve/create an index on the given columns by merging the given
* template information with any user-provided information.
*
* @param context the mapping we're retrieving an index for
* @param prefix localized error message key prefix
* @param tmplate template for expected index information
* @param cols the indexed columns
* @param adapt whether we can modify the existing mapping or schema
*/
protected Index createIndex(MetaDataContext context, String prefix,
Index tmplate, Column[] cols, boolean adapt) {
if (prefix == null)
prefix = "generic";
// can't create an index if there are no cols
if (cols == null || cols.length == 0) {
if (_idx != null)
throw new MetaDataException(_loc.get(prefix
+ "-no-index-cols", context));
return null;
}
// look for an existing index on these columns
Table table = cols[0].getTable();
Index[] idxs = table.getIndexes();
Index exist = null;
for (Index index : idxs) {
if (index.columnsMatch(cols)) {
exist = index;
break;
}
}
// remove existing index?
if (!_canIdx) {
if (exist == null)
return null;
if (!adapt)
throw new MetaDataException(_loc.get(prefix + "-index-exists",
context));
table.removeIndex(exist);
return null;
}
// if we have an existing index, merge given info into it
if (exist != null) {
if (_idx != null && _idx.isUnique() && !exist.isUnique()) {
if (!adapt)
throw new MetaDataException(_loc.get(prefix
+ "-index-not-unique", context));
exist.setUnique(true);
}
return exist;
}
// if no defaults return null
MappingRepository repos = (MappingRepository) context.getRepository();
boolean fill = repos.getMappingDefaults().defaultMissingInfo();
if (_idx == null && (tmplate == null || (!adapt && !fill)))
return null;
DBIdentifier name = DBIdentifier.NULL;
boolean unq;
if (_idx != null) {
name = _idx.getIdentifier();
unq = _idx.isUnique();
// preserve multiple columns if they are specified in the index
if (_idx.getColumns() != null && _idx.getColumns().length > 1)
cols = _idx.getColumns();
} else
unq = tmplate.isUnique();
// if no name provided by user info, make one
if (DBIdentifier.isNull(name)) {
if (tmplate != null)
name = tmplate.getIdentifier();
else {
name = cols[0].getIdentifier();
name = repos.getDBDictionary().getValidIndexName(name, table);
}
}
Index idx = table.addIndex(name);
idx.setUnique(unq);
idx.setColumns(cols);
return idx;
}
/**
* Retrieve/create a unique constraint on the given columns by merging the
* given template information with any user-provided information.
*
* @param context the mapping we're retrieving a constraint for
* @param prefix localized error message key prefix
* @param tmplate template for expected unique information
* @param cols the constraint columns
* @param adapt whether we can modify the existing mapping or schema
*/
protected Unique createUnique(MetaDataContext context, String prefix,
Unique tmplate, Column[] cols, boolean adapt) {
if (prefix == null)
prefix = "generic";
// can't create a constraint if there are no cols
if (cols == null || cols.length == 0) {
if (_unq != null || tmplate != null)
throw new MetaDataException(_loc.get(prefix
+ "-no-unique-cols", context));
return null;
}
// look for an existing constraint on these columns
Table table = cols[0].getTable();
Unique[] unqs = table.getUniques();
Unique exist = null;
for (Unique unique : unqs) {
if (unique.columnsMatch(cols)) {
exist = unique;
break;
}
}
// remove existing unique?
if (!_canUnq) {
if (exist == null)
return null;
if (!adapt)
throw new MetaDataException(_loc.get(prefix
+ "-unique-exists", context));
table.removeUnique(exist);
return null;
}
// no defaults; return existing constraint (if any)
if (tmplate == null && _unq == null)
return exist;
MappingRepository repos = (MappingRepository) context.getRepository();
if (exist != null) {
if (_unq != null && _unq.isDeferred() && !exist.isDeferred()) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-defer-unique", context));
}
return exist;
}
// dict can't handle unique constraints?
DBDictionary dict = repos.getDBDictionary();
if (_unq != null && !dict.supportsUniqueConstraints) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-unique-support", context));
return null;
}
boolean fill = repos.getMappingDefaults().defaultMissingInfo();
if (!adapt && !fill && _unq == null)
return null;
DBIdentifier name = DBIdentifier.NULL;
boolean deferred;
if (_unq != null) {
name = _unq.getIdentifier();
deferred = _unq.isDeferred();
} else {
name = tmplate.getIdentifier();
deferred = tmplate.isDeferred();
}
if (deferred && !dict.supportsDeferredConstraints) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-create-defer-unique",
context, dict.platform));
deferred = false;
}
if (DBIdentifier.isEmpty(name)) {
name = cols[0].getIdentifier();
name = repos.getDBDictionary().getValidUniqueName(name, table);
}
Unique unq = table.addUnique(name);
unq.setDeferred(deferred);
unq.setColumns(cols);
return unq;
}
/**
* Retrieve/create a foreign key (possibly logical) on the given columns
* by merging the given template information with any user-provided
* information.
*
* @param context the mapping we're retrieving a key for
* @param prefix localized error message key prefix
* @param given the columns given by the user
* @param def defaults provider
* @param table the table for the key
* @param cls type we're joining from
* @param rel target type we're joining to
* @param inversable whether the foreign key can be inversed
* @param adapt whether we can modify the existing mapping or schema
*/
protected ForeignKey createForeignKey(MetaDataContext context,
String prefix, List<Column> given, ForeignKeyDefaults def, Table table,
ClassMapping cls, ClassMapping rel, boolean inversable, boolean adapt) {
assertTable(context, table);
if (prefix == null)
prefix = "generic";
// collect the foreign key columns and their targets
Object[][] joins = createJoins(context, prefix, table, cls, rel,
given, def, inversable, adapt);
_join = JOIN_FORWARD;
// establish local table using any join between two columns; if we only
// find constant joins, then keep default local table (directionless)
Table local = table;
Table foreign = rel.getTable();
Table tmp;
boolean constant = false;
boolean localSet = false;
for (Object[] objects : joins) {
if (objects[1] instanceof Column) {
tmp = ((Column) objects[0]).getTable();
if (!localSet) {
local = tmp;
localSet = true;
}
else if (tmp != local)
throw new MetaDataException(_loc.get(prefix
+ "-mult-fk-tables", context, local, tmp));
foreign = ((Column) objects[1]).getTable();
if (objects[2] == Boolean.TRUE)
_join = JOIN_INVERSE;
}
else
constant = true;
}
// if this is not a constant join, look for existing foreign key
// on local columns
ForeignKey exist = null;
if (!constant && local.getForeignKeys().length > 0) {
Column[] cols = new Column[joins.length];
Column[] pks = new Column[joins.length];
for (int i = 0; i < joins.length; i++) {
cols[i] = (Column) joins[i][0];
pks[i] = (Column) joins[i][1];
}
ForeignKey[] fks = local.getForeignKeys();
for (ForeignKey fk : fks) {
if (fk.getConstantColumns().length == 0
&& fk.getConstantPrimaryKeyColumns().length == 0
&& fk.columnsMatch(cols, pks)) {
exist = fk;
break;
}
}
}
MappingRepository repos = (MappingRepository) context.getRepository();
DBDictionary dict = repos.getDBDictionary();
if (exist != null) {
// make existing key logical?
if (!_canFK) {
if (exist.getDeleteAction() != ForeignKey.ACTION_NONE && !adapt)
throw new MetaDataException(_loc.get(prefix
+ "-fk-exists", context));
exist.setDeleteAction(ForeignKey.ACTION_NONE);
}
if (_fk != null && _fk.isDeferred() && !exist.isDeferred()) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-defer-fk", context));
}
// allow user-given info to override existing key if we're adapting;
// template info cannot override existing key
if (adapt && _fk != null) {
if (_fk.getUpdateAction() != ForeignKey.ACTION_NONE)
exist.setUpdateAction(_fk.getUpdateAction());
if (_fk.getDeleteAction() != ForeignKey.ACTION_NONE)
exist.setDeleteAction(_fk.getDeleteAction());
}
setIOFromJoins(exist, joins);
return exist;
}
DBIdentifier name = DBIdentifier.NULL;
int delAction = ForeignKey.ACTION_NONE;
int upAction = ForeignKey.ACTION_NONE;
boolean deferred = false;
boolean fill = repos.getMappingDefaults().defaultMissingInfo();
ForeignKey tmplate = (def == null) ? null
: def.get(local, foreign, _join == JOIN_INVERSE);
if (_fk != null && (tmplate == null || (!adapt && !fill))) {
// if not adapting or no template info use given data
name = _fk.getIdentifier();
delAction = _fk.getDeleteAction();
upAction = _fk.getUpdateAction();
deferred = _fk.isDeferred();
} else if (_canFK && (adapt || fill)) {
if (_fk == null && tmplate != null) {
// no user given info; use template data
name = tmplate.getIdentifier();
delAction = tmplate.getDeleteAction();
upAction = tmplate.getUpdateAction();
deferred = tmplate.isDeferred();
} else if (_fk != null && tmplate != null) {
// merge user and template data, always letting user info win
name = _fk.getIdentifier();
if (DBIdentifier.isNull(name) && !DBIdentifier.isNull(tmplate.getIdentifier()))
name = tmplate.getIdentifier();
delAction = _fk.getDeleteAction();
if (delAction == ForeignKey.ACTION_NONE)
delAction = tmplate.getDeleteAction();
upAction = _fk.getUpdateAction();
if (upAction == ForeignKey.ACTION_NONE)
upAction = tmplate.getUpdateAction();
deferred = _fk.isDeferred();
}
}
if (!dict.supportsDeleteAction(delAction)
|| !dict.supportsUpdateAction(upAction)) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-unsupported-fk-action", context));
delAction = ForeignKey.ACTION_NONE;
upAction = ForeignKey.ACTION_NONE;
}
if (deferred && !dict.supportsDeferredConstraints) {
Log log = repos.getLog();
if (log.isWarnEnabled())
log.warn(_loc.get(prefix + "-create-defer-fk",
context, dict.platform));
deferred = false;
}
// create foreign key with merged info
ForeignKey fk = local.addForeignKey(name);
fk.setDeleteAction(delAction);
fk.setUpdateAction(upAction);
fk.setDeferred(deferred);
// add joins to key
Column col;
for (Object[] join : joins) {
col = (Column) join[0];
if (join[1] instanceof Column)
fk.join(col, (Column) join[1]);
else if ((join[2] == Boolean.TRUE) != (_join == JOIN_INVERSE))
fk.joinConstant(join[1], col);
else
fk.joinConstant(col, join[1]);
}
setIOFromJoins(fk, joins);
return fk;
}
/**
* Use the join information to populate our internal column I/O data.
*/
private void setIOFromJoins(ForeignKey fk, Object[][] joins) {
List<Column> cols = getColumns();
_io = null;
if (cols.isEmpty())
return;
int constIdx = 0;
int idx;
for (int i = 0; i < joins.length; i++) {
// const columns are indexed after std join columns in fk IO
if (joins[i][1]instanceof Column)
idx = i - constIdx;
else if ((joins[i][2] == Boolean.TRUE) == (_join == JOIN_INVERSE))
idx = fk.getColumns().length + constIdx++;
else
continue;
setIOFromColumnFlags(cols.get(i), idx);
}
}
/**
* Create or retrieve the foreign key joins.
*
* @param context the mapping we're retrieving a key for
* @param prefix localized error message key prefix
* @param table the table for the key
* @param cls type we're joining from, if applicable
* @param rel target type we're joining to
* @param given the columns given by the user
* @param def foreign key defaults provider
* @param inversable whether the foreign key can be inversed
* @param adapt whether we can modify the existing mapping or schema
* @return array of tuples where the first element is the
* local column (or in the case of a constant join the
* sole column), the second is the target column (or
* constant), and the third is {@link Boolean#TRUE} if
* this is an inverse join
*/
private Object[][] createJoins(MetaDataContext context,
String prefix, Table table, ClassMapping cls, ClassMapping rel,
List<Column> given, ForeignKeyDefaults def, boolean inversable, boolean adapt) {
MappingRepository repos = (MappingRepository) context.getRepository();
boolean fill = repos.getMappingDefaults().defaultMissingInfo();
Object[][] joins;
// if no columns given, just create mirrors of target columns
if (given.isEmpty()) {
if (!adapt && !fill)
throw new MetaDataException(_loc.get(prefix + "-no-fk-cols",
context));
Column[] targets = rel.getPrimaryKeyColumns();
joins = new Object[targets.length][3];
Column tmplate;
for (int i = 0; i < targets.length; i++) {
tmplate = new Column();
tmplate.setIdentifier(targets[i].getIdentifier());
tmplate.setJavaType(targets[i].getJavaType());
tmplate.setType(targets[i].getType());
tmplate.setTypeName(targets[i].getTypeName());
tmplate.setSize(targets[i].getSize());
tmplate.setDecimalDigits(targets[i].getDecimalDigits());
if (def != null)
def.populate(table, rel.getTable(), tmplate, targets[i],
false, i, targets.length);
joins[i][0] = mergeColumn(context, prefix, tmplate, true,
null, table, adapt, fill);
joins[i][1] = targets[i];
}
return joins;
}
// use given columns to create join. we don't try to use any of the
// template columns, even if the user doesn't give a column linking to
// every primary key of the target type -- users are allowed to create
// partial joins. this means, though, that if a user wants to specify
// info for one join column, he has to at least create elements for
// all of them
joins = new Object[given.size()][3];
Column col;
for (int i = 0; i < joins.length; i++) {
col = given.get(i);
mergeJoinColumn(context, prefix, col, joins, i, table, cls, rel,
def, inversable && !col.getFlag(Column.FLAG_PK_JOIN), adapt,
fill);
}
return joins;
}
/**
* Create or retrieve a foreign key column for a join.
*
* @param context the mapping we're retrieving a key for
* @param prefix localized error message key prefix
* @param given the given local foreign key column
* @param joins array of joins
* @param idx index of the join array to populate
* @param table the table for the key
* @param cls the type we're joining from
* @param rel target type we're joining to
* @param def foreign key defaults provider;
* use null to mirror target column names
* @param inversable whether the foreign key can be inversed
* @param adapt whether we can modify the existing mapping or schema
* @param fill whether to default missing column information
*/
private void mergeJoinColumn(MetaDataContext context, String prefix,
Column given, Object[][] joins, int idx, Table table, ClassMapping cls,
ClassMapping rel, ForeignKeyDefaults def, boolean inversable,
boolean adapt, boolean fill) {
// default to the primary key column name if this is a pk join
DBIdentifier name = given.getIdentifier();
if (DBIdentifier.isNull(name) && given.getFlag(Column.FLAG_PK_JOIN) && cls != null) {
Column[] pks = cls.getPrimaryKeyColumns();
if (pks.length == 1)
name = pks[0].getIdentifier();
}
// if we can't adapt, then the user must at least give a column name
if (DBIdentifier.isNull(name) && !adapt && !fill)
throw new MetaDataException(_loc.get(prefix + "-no-fkcol-name",
context));
// check to see if the column isn't in the expected table; it might
// be an inverse join or a join to a base class of the target type
Table local = table;
Table foreign = rel.getTable();
boolean fullName = false;
boolean inverse = false;
if (!DBIdentifier.isNull(name)) {
QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(name);
if (!DBIdentifier.isNull(path.getObjectTableName())) {
if (DBIdentifier.isEmpty(path.getObjectTableName()))
local = foreign;
else
local = findTable(context, path.getObjectTableName(),
local, foreign, null);
fullName = true;
name = path.getIdentifier().getUnqualifiedName();
// if inverse join, then swap local and foreign tables
if (local != table) {
foreign = table;
inverse = true;
}
}
}
boolean forceInverse = !fullName && _join == JOIN_INVERSE;
if (forceInverse) {
local = foreign;
foreign = table;
inverse = true;
}
// determine target
DBIdentifier targetName = given.getTargetIdentifier();
Object target = null;
Table ttable = null;
boolean constant = false;
boolean fullTarget = false;
if (DBIdentifier.isNull(targetName) && given.getTargetField() != null) {
ClassMapping tcls = (inverse) ? cls : rel;
String fieldName = given.getTargetField();
String[] names = Normalizer.splitName(fieldName);
fullTarget = names.length > 1;
if (names.length > 1 && StringUtil.isEmpty(names[0])) {
// allow use of '.' without prefix to mean "use expected local
// cls"; but if we already inversed no need to switch again
if (!inverse)
tcls = cls;
fieldName = names[1];
} else if (names.length > 1) {
// must be class + field name
tcls = findClassMapping(context, names[0], cls, rel);
fieldName = names[1];
}
if (tcls == null)
throw new MetaDataException(_loc.get(prefix
+ "-bad-fktargetcls", context, fieldName, name));
FieldMapping field = tcls.getFieldMapping(fieldName);
if (field == null)
throw new MetaDataException(_loc.get(prefix
+ "-bad-fktargetfield", new Object[]{ context, fieldName,
name, tcls }));
if (field.getColumns().length != 1)
throw new MetaDataException(_loc.get(prefix
+ "-fktargetfield-cols", context, fieldName, name));
ttable = (field.getJoinForeignKey() != null) ? field.getTable()
: field.getDefiningMapping().getTable();
targetName = field.getColumns()[0].getIdentifier();
} else if (!DBIdentifier.isNull(targetName)) {
String targetNameStr = targetName.getName();
if (targetNameStr.charAt(0) == '\'') {
constant = true;
target = targetNameStr.substring(1, targetNameStr.length() - 1);
} else if (targetNameStr.charAt(0) == '-'
|| targetNameStr.charAt(0) == '.'
|| Character.isDigit(targetNameStr.charAt(0))) {
constant = true;
try {
if (targetNameStr.indexOf('.') == -1)
target = new Integer(targetNameStr);
else
target = new Double(targetNameStr);
} catch (RuntimeException re) {
throw new MetaDataException(_loc.get(prefix
+ "-bad-fkconst", context, targetName, name));
}
} else if ("null".equalsIgnoreCase(targetNameStr))
constant = true;
else {
QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(targetName);
fullTarget = (!DBIdentifier.isNull(path.getObjectTableName()));
if (!DBIdentifier.isNull(path.getObjectTableName()) &&
DBIdentifier.isEmpty(path.getObjectTableName())) {
// allow use of '.' without prefix to mean "use expected
// local table", but ignore if we're already inversed
if (!inverse)
ttable = local;
targetName = path.getIdentifier().getUnqualifiedName();
} else if (!DBIdentifier.isNull(path.getObjectTableName())) {
ttable = findTable(context, path.getObjectTableName(), foreign, local, (inverse) ? cls : rel);
targetName = path.getIdentifier().getUnqualifiedName();
}
}
}
// use explicit target table if available
if (ttable == local && local != foreign) {
// swap, unless user gave incompatible table in column name
if (fullName)
throw new MetaDataException(_loc.get(prefix
+ "-bad-fktarget-inverse", new Object[]{ context, name,
foreign, ttable }));
local = foreign;
foreign = ttable;
} else if (ttable != null) {
// ttable might be a table of a base class of the target
foreign = ttable;
}
// check to see if we inversed; if this is a same-table join, then
// consider it an implicit inverse if the user includes the table name
// in the column name, but not in the column target, or if the user
// gives no column name but a full target name
inverse = inverse || local != table || (local == foreign
&& ((fullName && !fullTarget) || (DBIdentifier.isNull(name) && fullTarget)));
if (!inversable && !constant && inverse) {
if (local == foreign)
throw new MetaDataException(_loc.get(prefix
+ "-bad-fk-self-inverse", context, local));
throw new MetaDataException(_loc.get(prefix + "-bad-fk-inverse",
context, local, table));
}
if (DBIdentifier.isNull(name) && constant)
throw new MetaDataException(_loc.get(prefix
+ "-no-fkcol-name-adapt", context));
if (DBIdentifier.isNull(name) && DBIdentifier.isNull(targetName)) {
// if no name or target is provided and there's more than one likely
// join possibility, too ambiguous
PrimaryKey pk = foreign.getPrimaryKey();
if (joins.length != 1 || pk == null || pk.getColumns().length != 1)
throw new MetaDataException(_loc.get(prefix
+ "-no-fkcol-name-adapt", context));
// assume target is pk column
targetName = pk.getColumns()[0].getIdentifier();
} else if (!DBIdentifier.isNull(name) && DBIdentifier.isNull(targetName)) {
// if one primary key column use it for target; if multiple joins
// look for a foreign column with same name as local column
PrimaryKey pk = foreign.getPrimaryKey();
if (joins.length == 1 && pk != null && pk.getColumns().length == 1) {
targetName = pk.getColumns()[0].getIdentifier();
}
else if (foreign.getColumn(name) != null) {
targetName = name;
}
else {
throw new MetaDataException(_loc.get(prefix
+ "-no-fkcol-target-adapt", context, name));
}
}
// find the target column, and create template for local column based
// on it
Column tmplate = new Column();
tmplate.setIdentifier(name);
if (!constant) {
Column tcol = foreign.getColumn(targetName, false);
if (tcol == null) {
String schemaCase = rel.getMappingRepository().getDBDictionary().schemaCase;
if (DBDictionary.SCHEMA_CASE_LOWER.equals(schemaCase)) {
tcol = foreign.getColumn(DBIdentifier.toLower(targetName, true), false);
} else if (DBDictionary.SCHEMA_CASE_UPPER.equals(schemaCase)) {
tcol = foreign.getColumn(DBIdentifier.toUpper(targetName, true), false);
}
}
if (tcol == null) {
// give up
throw new MetaDataException(_loc.get(prefix + "-bad-fktarget",
new Object[]{ context, targetName, name, foreign }));
}
if (DBIdentifier.isNull(name))
tmplate.setIdentifier(tcol.getIdentifier());
tmplate.setJavaType(tcol.getJavaType());
tmplate.setType(tcol.getType());
tmplate.setTypeName(tcol.getTypeName());
tmplate.setSize(tcol.getSize());
tmplate.setDecimalDigits(tcol.getDecimalDigits());
target = tcol;
} else if (target instanceof String)
tmplate.setJavaType(JavaTypes.STRING);
else if (target instanceof Integer)
tmplate.setJavaType(JavaTypes.INT);
else if (target instanceof Double)
tmplate.setJavaType(JavaTypes.DOUBLE);
// populate template, but let user-given name override default name
if (def != null)
def.populate(local, foreign, tmplate, target, inverse, idx,
joins.length);
if (!DBIdentifier.isNull(name))
tmplate.setIdentifier(name);
// create or merge local column
Column col = mergeColumn(context, prefix, tmplate, true, given, local,
adapt, fill);
joins[idx][0] = col;
joins[idx][1] = target;
if (inverse)
joins[idx][2] = Boolean.TRUE;
}
/**
* Find the target class mapping given the user's class name.
*
* @param context for error messages
* @param clsName class name given by user
* @param cls original source mapping
* @param rel original target mapping
*/
private static ClassMapping findClassMapping(MetaDataContext context,
String clsName, ClassMapping cls, ClassMapping rel) {
if (isClassMappingName(clsName, cls))
return cls;
if (isClassMappingName(clsName, rel))
return rel;
throw new MetaDataException(_loc.get("target-wrong-cls", new Object[]
{ context, clsName, cls, rel }));
}
/**
* Return whether the given name matches the given mapping.
*/
private static boolean isClassMappingName(String name, ClassMapping cls) {
if (cls == null)
return false;
if (name.equals(cls.getDescribedType().getName())
|| name.equals(ClassUtil.getClassName(cls.getDescribedType())))
return true;
return isClassMappingName(name, cls.getPCSuperclassMapping());
}
/**
* Sets internal column information to match the given mapped columns.
*
* @param forceJDBCType whether to force the jdbc-type of the columns
* to be set, even when it matches the default for the columns' java type
*/
protected void syncColumns(MetaDataContext context, Column[] cols,
boolean forceJDBCType) {
if (cols == null || cols.length == 0)
_cols = null;
else {
_cols = new ArrayList<>(cols.length);
Column col;
for (int i = 0; i < cols.length; i++) {
col = syncColumn(context, cols[i], cols.length,
forceJDBCType, cols[i].getTable(), null, null, false);
setColumnFlagsFromIO(col, i);
_cols.add(col);
}
}
}
/**
* Set I/O flags on the column.
*/
private void setColumnFlagsFromIO(Column col, int i) {
if (_io == null)
return;
col.setFlag(Column.FLAG_UNUPDATABLE, !_io.isUpdatable(i, false));
col.setFlag(Column.FLAG_UNINSERTABLE, !_io.isInsertable(i, false));
}
/**
* Sets internal index information to match given mapped index.
*/
protected void syncIndex(MetaDataContext context, Index idx) {
if (idx == null) {
_idx = null;
return;
}
_canIdx = true;
_idx = new Index();
_idx.setIdentifier(idx.getIdentifier());
_idx.setUnique(idx.isUnique());
if (idx.getColumns() != null && idx.getColumns().length > 1)
_idx.setColumns(idx.getColumns());
}
/**
* Sets internal constraint information to match given mapped constraint.
*/
protected void syncUnique(MetaDataContext context, Unique unq) {
if (unq == null) {
_unq = null;
return;
}
_canUnq = true;
_unq = new Unique();
_unq.setIdentifier(unq.getIdentifier());
_unq.setDeferred(unq.isDeferred());
}
/**
* Sets internal constraint and column information to match given mapped
* constraint.
*
* @param local default local table
* @param target default target table
*/
protected void syncForeignKey(MetaDataContext context, ForeignKey fk,
Table local, Table target) {
if (fk == null) {
_fk = null;
_cols = null;
_join = JOIN_NONE;
return;
}
if (_join == JOIN_NONE)
_join = JOIN_FORWARD;
if (fk.isLogical())
_fk = null;
else {
_canFK = true;
_fk = new ForeignKey();
_fk.setIdentifier(fk.getIdentifier());
_fk.setDeleteAction(fk.getDeleteAction());
_fk.setUpdateAction(fk.getUpdateAction());
_fk.setDeferred(fk.isDeferred());
}
Column[] cols = fk.getColumns();
Column[] pkCols = fk.getPrimaryKeyColumns();
Column[] ccols = fk.getConstantColumns();
Object[] cs = fk.getConstants();
Column[] cpkCols = fk.getConstantPrimaryKeyColumns();
Object[] cpks = fk.getPrimaryKeyConstants();
int size = cols.length + ccols.length + cpkCols.length;
_cols = new ArrayList<>(size);
Column col;
for (int i = 0; i < cols.length; i++) {
col = syncColumn(context, cols[i], size, false, local,
target, pkCols[i], _join == JOIN_INVERSE);
setColumnFlagsFromIO(col, i);
_cols.add(col);
}
Object constant;
for (int i = 0; i < ccols.length; i++) {
constant = (cs[i] == null) ? NULL : cs[i];
col = syncColumn(context, ccols[i], size, false, local,
target, constant, _join == JOIN_INVERSE);
setColumnFlagsFromIO(col, cols.length + i);
_cols.add(col);
}
for (int i = 0; i < cpkCols.length; i++) {
constant = (cpks[i] == null) ? NULL : cpks[i];
_cols.add(syncColumn(context, cpkCols[i], size, false, target,
local, constant, _join != JOIN_INVERSE));
}
}
/**
* Create a copy of the given column with the raw mapping information
* set correctly, and without settings that match defaults.
*
* @param num the number of columns for this mapping
* @param forceJDBCType whether the jdbc-type of the created column
* should be set, even if it matches the default
* for the given column's java type
* @param colTable expected table for the column
* @param targetTable expected target table for join column
* @param target target column or object for join column; for a
* constant null target, use {@link #NULL}
* @param inverse whether join column is for inverse join
*/
protected static Column syncColumn(MetaDataContext context, Column col,
int num, boolean forceJDBCType, Table colTable, Table targetTable,
Object target, boolean inverse) {
// use full name for cols that aren't in the expected table, or that
// are inverse joins
DBDictionary dict = ((MappingRepository) context.getRepository()).
getDBDictionary();
Column copy = new Column();
if (col.getTable() != colTable || inverse)
copy.setIdentifier(QualifiedDBIdentifier.newPath(dict.getFullIdentifier(col.getTable(), true),
col.getIdentifier()));
else
copy.setIdentifier(col.getIdentifier());
// set target if not default
if (target != null) {
if (target == NULL)
copy.setTargetIdentifier(DBIdentifier.newColumn("null"));
else if (target instanceof Column) {
Column tcol = (Column) target;
if ((!inverse && tcol.getTable() != targetTable)
|| (inverse && tcol.getTable() != colTable))
copy.setTargetIdentifier(
QualifiedDBIdentifier.newPath(dict.getFullIdentifier(tcol.getTable(), true),
tcol.getIdentifier()));
else if (!defaultTarget(col, tcol, num))
copy.setTargetIdentifier(tcol.getIdentifier());
} else if (target instanceof Number)
copy.setTargetIdentifier(DBIdentifier.newConstant(target.toString()));
else
copy.setTargetIdentifier(DBIdentifier.newConstant("'" + target + "'"));
} else if (num > 1)
copy.setTargetField(col.getTargetField());
if (col.getSize() != 0 && col.getSize() != dict.characterColumnSize
&& (col.getSize() != -1 || !col.isLob()))
copy.setSize(col.getSize());
if (col.getDecimalDigits() != 0)
copy.setDecimalDigits(col.getDecimalDigits());
if (col.getDefaultString() != null)
copy.setDefaultString(col.getDefaultString());
if (col.isNotNull() && !col.isPrimaryKey()
&& (!isPrimitive(col.getJavaType()) || isForeignKey(col)))
copy.setNotNull(true);
// set type name if not default
String typeName = col.getTypeName();
if (typeName != null || copy.getSize() != 0
|| copy.getDecimalDigits() != 0) {
// is this the dict default type? have to ensure jdbc-type set
// prior to finding dict default
copy.setType(col.getType());
String defName = dict.getTypeName(copy);
copy.setType(Types.OTHER);
// copy should not have size info set if it isn't used in type name
boolean defSized = defName.indexOf('(') != -1;
if (!defSized) {
if (copy.getSize() > 0)
copy.setSize(0);
copy.setDecimalDigits(0);
}
if (typeName != null) {
// make sure to strip size for comparison
if (typeName.indexOf('(') == -1 && defSized)
defName = defName.substring(0, defName.indexOf('('));
if (!typeName.equalsIgnoreCase(defName))
copy.setTypeName(typeName);
}
}
// set jdbc-type if not default or if forced
if (forceJDBCType
|| (target != null && !(target instanceof Column)
&& col.getType() != Types.VARCHAR)
|| dict.getJDBCType(col.getJavaType(), false) != col.getType())
copy.setType(col.getType());
return copy;
}
/**
* Return whether the given column belongs to a foreign key.
*/
private static boolean isForeignKey(Column col)
{
if (col.getTable() == null)
return false;
ForeignKey[] fks = col.getTable().getForeignKeys();
for (ForeignKey fk : fks)
if (fk.containsColumn(col)
|| fk.containsConstantColumn(col))
return true;
return false;
}
/**
* Return true if the given type code represents a primitive.
*/
private static boolean isPrimitive(int type) {
switch (type) {
case JavaTypes.BOOLEAN:
case JavaTypes.BYTE:
case JavaTypes.CHAR:
case JavaTypes.DOUBLE:
case JavaTypes.FLOAT:
case JavaTypes.INT:
case JavaTypes.LONG:
case JavaTypes.SHORT:
return true;
default:
return false;
}
}
/**
* Return true if the given target column matches the default.
* If there is only one column involved in the join and it links to the
* single target table pk column, or if the column name is the same as
* the target column name, then the target is the default.
*/
private static boolean defaultTarget(Column col, Column targetCol,
int num) {
if (col.getIdentifier().equals(targetCol.getIdentifier()))
return true;
if (num > 1)
return false;
PrimaryKey pk = targetCol.getTable().getPrimaryKey();
if (pk == null || pk.getColumns().length != 1)
return false;
return targetCol == pk.getColumns()[0];
}
/**
* Supplies default table information.
*/
public interface TableDefaults {
/**
* Return the default table name.
* @deprecated
*/
@Deprecated String get(Schema schema);
DBIdentifier getIdentifier(Schema schema);
}
/**
* Supplies default foreign key information.
*/
public interface ForeignKeyDefaults {
/**
* Return a default foreign key for the given tables, or null to
* create a logical foreign key only. Do not fill in the columns of
* the foreign key, only attributes like its name, delete action, etc.
* Do not add the foreign key to the table.
*/
ForeignKey get(Table local, Table foreign, boolean inverse);
/**
* Populate the given foreign key column with defaults.
*
* @param target the target column or constant value
* @param pos the index of this column in the foreign key
* @param cols the number of columns in the foreign key
*/
void populate(Table local, Table foreign, Column col,
Object target, boolean inverse, int pos, int cols);
}
}