blob: 40df305eadc2f9951ba53d02b66afaed44acc9cc [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.schema;
import java.sql.DatabaseMetaData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
import org.apache.commons.lang.ObjectUtils;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringDistance;
import org.apache.openjpa.util.InvalidStateException;
/**
* Represents a database foreign key; may be a logical key with no
* database representation. This class can also represent a partial key,
* aligning with {@link DatabaseMetaData}.
*
* @author Abe White
*/
public class ForeignKey
extends Constraint {
/**
* Logical foreign key; links columns, but does not perform any action
* when the joined primary key columns are modified.
*/
public static final int ACTION_NONE = 1;
/**
* Throw an exception if joined primary key columns are modified.
*/
public static final int ACTION_RESTRICT = 2;
/**
* Cascade any modification of the joined primary key columns to
* this table. If the joined primary key row is deleted, the row in this
* table will also be deleted.
*/
public static final int ACTION_CASCADE = 3;
/**
* Null the local columns if the joined primary key columns are modified.
*/
public static final int ACTION_NULL = 4;
/**
* Set the local columns to their default values if the primary key
* columns are modified.
*/
public static final int ACTION_DEFAULT = 5;
private static final Localizer _loc =
Localizer.forPackage(ForeignKey.class);
private String _pkTableName = null;
private String _pkSchemaName = null;
private String _pkColumnName = null;
private int _seq = 0;
private LinkedHashMap _joins = null;
private LinkedHashMap _joinsPK = null;
private LinkedHashMap _consts = null;
private LinkedHashMap _constsPK = null;
private int _delAction = ACTION_NONE;
private int _upAction = ACTION_NONE;
private int _index = 0;
// cached items
private Column[] _locals = null;
private Column[] _pks = null;
private Object[] _constVals = null;
private Column[] _constCols = null;
private Object[] _constValsPK = null;
private Column[] _constColsPK = null;
private Table _pkTable = null;
private Boolean _autoAssign = null;
/**
* Return the foreign key action constant for the given action name.
*/
public static int getAction(String name) {
if (name == null || "none".equalsIgnoreCase(name))
return ACTION_NONE;
if ("cascade".equalsIgnoreCase(name))
return ACTION_CASCADE;
if ("default".equalsIgnoreCase(name))
return ACTION_DEFAULT;
if ("restrict".equalsIgnoreCase(name)
|| "exception".equalsIgnoreCase(name))
return ACTION_RESTRICT;
if ("null".equalsIgnoreCase(name))
return ACTION_NULL;
// not a recognized action; check for typo
List recognized = Arrays.asList(new String[]{ "none", "exception",
"restrict", "cascade", "null", "default", });
String closest = StringDistance.getClosestLevenshteinDistance(name,
recognized, .5F);
String msg;
if (closest != null)
msg = _loc.get("bad-fk-action-hint", name, closest, recognized)
.getMessage();
else
msg = _loc.get("bad-fk-action", name, recognized).getMessage();
throw new IllegalArgumentException(msg);
}
/**
* Return the foreign key action name for the given action constant.
*/
public static String getActionName(int action) {
switch (action) {
case ACTION_NONE:
return "none";
case ACTION_RESTRICT:
return "restrict";
case ACTION_CASCADE:
return "cascade";
case ACTION_DEFAULT:
return "default";
case ACTION_NULL:
return "null";
default:
throw new IllegalArgumentException(String.valueOf(action));
}
}
/**
* Default constructor.
*/
public ForeignKey() {
}
/**
* Constructor.
*
* @param name the foreign key name, if any
* @param table the local table of the foreign key
*/
public ForeignKey(String name, Table table) {
super(name, table);
}
public boolean isLogical() {
return _delAction == ACTION_NONE;
}
/**
* Whether the primary key columns of this key are auto-incrementing, or
* whether they themselves are members of a foreign key who's primary key
* is auto-incrementing (recursing to arbitrary depth).
*/
public boolean isPrimaryKeyAutoAssigned() {
if (_autoAssign != null)
return _autoAssign.booleanValue();
return isPrimaryKeyAutoAssigned(new ArrayList(3));
}
/**
* Helper to calculate whether this foreign key depends on auto-assigned
* columns. Recurses appropriately if the primary key columns this key
* joins to are themselves members of a foreign key that is dependent on
* auto-assigned columns. Caches calculated auto-assign value as a side
* effect.
*
* @param seen track seen foreign keys to prevent infinite recursion in
* the case of foreign key cycles
*/
private boolean isPrimaryKeyAutoAssigned(List seen) {
if (_autoAssign != null)
return _autoAssign.booleanValue();
Column[] cols = getPrimaryKeyColumns();
if (cols.length == 0) {
_autoAssign = Boolean.FALSE;
return false;
}
for (int i = 0; i < cols.length; i++) {
if (cols[i].isAutoAssigned()) {
_autoAssign = Boolean.TRUE;
return true;
}
}
ForeignKey[] fks = _pkTable.getForeignKeys();
seen.add(this);
for (int i = 0; i < cols.length; i++) {
for (int j = 0; j < fks.length; j++) {
if (!fks[j].containsColumn(cols[i]))
continue;
if (!seen.contains(fks[j])
&& fks[j].isPrimaryKeyAutoAssigned(seen)) {
_autoAssign = Boolean.TRUE;
return true;
}
}
}
_autoAssign = Boolean.FALSE;
return false;
}
/**
* The name of the primary key table.
*/
public String getPrimaryKeyTableName() {
Table table = getPrimaryKeyTable();
if (table != null)
return table.getName();
return _pkTableName;
}
/**
* The name of the primary key table. You can only set the primary
* key table name on foreign keys that have not already been joined.
*/
public void setPrimaryKeyTableName(String pkTableName) {
if (getPrimaryKeyTable() != null)
throw new IllegalStateException();
_pkTableName = pkTableName;
}
/**
* The name of the primary key table's schema.
*/
public String getPrimaryKeySchemaName() {
Table table = getPrimaryKeyTable();
if (table != null)
return table.getSchemaName();
return _pkSchemaName;
}
/**
* The name of the primary key table's schema. You can only set the
* primary key schema name on foreign keys that have not already been
* joined.
*/
public void setPrimaryKeySchemaName(String pkSchemaName) {
if (getPrimaryKeyTable() != null)
throw new IllegalStateException();
_pkSchemaName = pkSchemaName;
}
/**
* The name of the primary key column.
*/
public String getPrimaryKeyColumnName() {
return _pkColumnName;
}
/**
* The name of the primary key column. You can only set the
* primary key column name on foreign keys that have not already been
* joined.
*/
public void setPrimaryKeyColumnName(String pkColumnName) {
if (getPrimaryKeyTable() != null)
throw new IllegalStateException();
_pkColumnName = pkColumnName;
}
/**
* The sequence of this join in the foreign key.
*/
public int getKeySequence() {
return _seq;
}
/**
* The sequence of this join in the foreign key.
*/
public void setKeySequence(int seq) {
_seq = seq;
}
/**
* Return the delete action for the key. Will be one of:
* {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
* {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
*/
public int getDeleteAction() {
return _delAction;
}
/**
* Set the delete action for the key. Must be one of:
* {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
* {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
*/
public void setDeleteAction(int action) {
_delAction = action;
if (action == ACTION_NONE)
_upAction = ACTION_NONE;
else if (_upAction == ACTION_NONE)
_upAction = ACTION_RESTRICT;
}
/**
* Return the update action for the key. Will be one of:
* {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
* {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
*/
public int getUpdateAction() {
return _upAction;
}
/**
* Set the update action for the key. Must be one of:
* {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
* {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
*/
public void setUpdateAction(int action) {
_upAction = action;
if (action == ACTION_NONE)
_delAction = ACTION_NONE;
else if (_delAction == ACTION_NONE)
_delAction = ACTION_RESTRICT;
}
/**
* Return the foreign key's 0-based index in the owning table.
*/
public int getIndex() {
Table table = getTable();
if (table != null)
table.indexForeignKeys();
return _index;
}
/**
* Set the foreign key's 0-based index in the owning table.
*/
void setIndex(int index) {
_index = index;
}
/**
* Return the primary key column joined to the given local column.
*/
public Column getPrimaryKeyColumn(Column local) {
return (_joins == null) ? null : (Column) _joins.get(local);
}
/**
* Return the local column joined to the given primary key column.
*/
public Column getColumn(Column pk) {
return (_joinsPK == null) ? null : (Column) _joinsPK.get(pk);
}
/**
* Return the constant value assigned to the given local column.
*/
public Object getConstant(Column local) {
return (_consts == null) ? null : _consts.get(local);
}
/**
* Return the constant value assigned to the given primary key column.
*/
public Object getPrimaryKeyConstant(Column pk) {
return (_constsPK == null) ? null : _constsPK.get(pk);
}
/**
* Return the local columns in the foreign key local table order.
*/
public Column[] getColumns() {
if (_locals == null)
_locals = (_joins == null) ? Schemas.EMPTY_COLUMNS : (Column[])
_joins.keySet().toArray(new Column[_joins.size()]);
return _locals;
}
/**
* Return the constant values assigned to the local columns
* returned by {@link #getConstantColumns}.
*/
public Object[] getConstants() {
if (_constVals == null)
_constVals = (_consts == null) ? Schemas.EMPTY_VALUES
: _consts.values().toArray();
return _constVals;
}
/**
* Return the constant values assigned to the primary key columns
* returned by {@link #getConstantPrimaryKeyColumns}.
*/
public Object[] getPrimaryKeyConstants() {
if (_constValsPK == null)
_constValsPK = (_constsPK == null) ? Schemas.EMPTY_VALUES
: _constsPK.values().toArray();
return _constValsPK;
}
/**
* Return true if the fk includes the given local column.
*/
public boolean containsColumn(Column col) {
return _joins != null && _joins.containsKey(col);
}
/**
* Return true if the fk includes the given primary key column.
*/
public boolean containsPrimaryKeyColumn(Column col) {
return _joinsPK != null && _joinsPK.containsKey(col);
}
/**
* Return true if the fk includes the given local column.
*/
public boolean containsConstantColumn(Column col) {
return _consts != null && _consts.containsKey(col);
}
/**
* Return true if the fk includes the given primary key column.
*/
public boolean containsConstantPrimaryKeyColumn(Column col) {
return _constsPK != null && _constsPK.containsKey(col);
}
/**
* Return the foreign columns in the foreign key, in join-order with
* the result of {@link #getColumns}.
*/
public Column[] getPrimaryKeyColumns() {
if (_pks == null)
_pks = (_joins == null) ? Schemas.EMPTY_COLUMNS : (Column[])
_joins.values().toArray(new Column[_joins.size()]);
return _pks;
}
/**
* Return the local columns that we link to using constant values.
*/
public Column[] getConstantColumns() {
if (_constCols == null)
_constCols = (_consts == null) ? Schemas.EMPTY_COLUMNS : (Column[])
_consts.keySet().toArray(new Column[_consts.size()]);
return _constCols;
}
/**
* Return the primary key columns that we link to using constant values.
*/
public Column[] getConstantPrimaryKeyColumns() {
if (_constColsPK == null)
_constColsPK = (_constsPK == null) ? Schemas.EMPTY_COLUMNS :
(Column[]) _constsPK.keySet().toArray
(new Column[_constsPK.size()]);
return _constColsPK;
}
/**
* Set the foreign key's joins.
*/
public void setJoins(Column[] cols, Column[] pkCols) {
Column[] cur = getColumns();
for (int i = 0; i < cur.length; i++)
removeJoin(cur[i]);
if (cols != null)
for (int i = 0; i < cols.length; i++)
join(cols[i], pkCols[i]);
}
/**
* Set the foreign key's constant joins.
*/
public void setConstantJoins(Object[] consts, Column[] pkCols) {
Column[] cur = getConstantPrimaryKeyColumns();
for (int i = 0; i < cur.length; i++)
removeJoin(cur[i]);
if (consts != null)
for (int i = 0; i < consts.length; i++)
joinConstant(consts[i], pkCols[i]);
}
/**
* Set the foreign key's constant joins.
*/
public void setConstantJoins(Column[] cols, Object[] consts) {
Column[] cur = getConstantColumns();
for (int i = 0; i < cur.length; i++)
removeJoin(cur[i]);
if (consts != null)
for (int i = 0; i < consts.length; i++)
joinConstant(cols[i], consts[i]);
}
/**
* Join a local column to a primary key column of another table.
*/
public void join(Column local, Column toPK) {
if (!ObjectUtils.equals(local.getTable(), getTable()))
throw new InvalidStateException(_loc.get("table-mismatch",
local.getTable(), getTable()));
Table pkTable = toPK.getTable();
if (_pkTable != null && !_pkTable.equals(pkTable))
throw new InvalidStateException(_loc.get("fk-mismatch",
pkTable, _pkTable));
_pkTable = pkTable;
if (_joins == null)
_joins = new LinkedHashMap();
_joins.put(local, toPK);
if (_joinsPK == null)
_joinsPK = new LinkedHashMap();
_joinsPK.put(toPK, local);
// force re-cache
_locals = null;
_pks = null;
if (_autoAssign == Boolean.FALSE)
_autoAssign = null;
}
/**
* Join a constant value to a primary key column of another table. The
* constant must be either a string or a number.
*/
public void joinConstant(Object val, Column toPK) {
Table pkTable = toPK.getTable();
if (_pkTable != null && !_pkTable.equals(pkTable))
throw new InvalidStateException(_loc.get("fk-mismatch",
pkTable, _pkTable));
_pkTable = pkTable;
if (_constsPK == null)
_constsPK = new LinkedHashMap();
_constsPK.put(toPK, val);
// force re-cache
_constValsPK = null;
_constColsPK = null;
}
/**
* Join a constant value to a local column of this table. The
* constant must be either a string or a number.
*/
public void joinConstant(Column col, Object val) {
if (_consts == null)
_consts = new LinkedHashMap();
_consts.put(col, val);
// force re-cache
_constVals = null;
_constCols = null;
}
/**
* Remove any joins inolving the given column.
*
* @return true if the join was removed, false if not part of the key
*/
public boolean removeJoin(Column col) {
boolean remd = false;
Object rem;
if (_joins != null) {
rem = _joins.remove(col);
if (rem != null) {
_locals = null;
_pks = null;
_joinsPK.remove(rem);
remd = true;
}
}
if (_joinsPK != null) {
rem = _joinsPK.remove(col);
if (rem != null) {
_locals = null;
_pks = null;
_joins.remove(rem);
remd = true;
}
}
if (_consts != null) {
if (_consts.remove(col) != null) {
_constVals = null;
_constCols = null;
remd = true;
}
}
if (_constsPK != null) {
if (_constsPK.containsKey(col)) {
_constsPK.remove(col);
_constValsPK = null;
_constColsPK = null;
remd = true;
}
}
if ((_joins == null || _joins.isEmpty())
&& (_constsPK == null || _constsPK.isEmpty()))
_pkTable = null;
if (remd && _autoAssign == Boolean.TRUE)
_autoAssign = null;
return remd;
}
/**
* Returns the table this foreign key is linking to, if it is known yet.
*/
public Table getPrimaryKeyTable() {
return _pkTable;
}
/**
* Ref all columns in this key.
*/
public void refColumns() {
Column[] cols = getColumns();
for (int i = 0; i < cols.length; i++)
cols[i].ref();
cols = getConstantColumns();
for (int i = 0; i < cols.length; i++)
cols[i].ref();
}
/**
* Deref all columns in this key.
*/
public void derefColumns() {
Column[] cols = getColumns();
for (int i = 0; i < cols.length; i++)
cols[i].deref();
cols = getConstantColumns();
for (int i = 0; i < cols.length; i++)
cols[i].deref();
}
/**
* Foreign keys are equal if the satisfy the equality constraints of
* {@link Constraint} and they have the same local and primary key
* columns and action.
*/
public boolean equalsForeignKey(ForeignKey fk) {
if (fk == this)
return true;
if (fk == null)
return false;
if (getDeleteAction() != fk.getDeleteAction())
return false;
if (isDeferred() != fk.isDeferred())
return false;
if (!columnsMatch(fk.getColumns(), fk.getPrimaryKeyColumns()))
return false;
if (!match(getConstantColumns(), fk.getConstantColumns()))
return false;
if (!match(getConstants(), fk.getConstants()))
return false;
if (!match(getConstantPrimaryKeyColumns(),
fk.getConstantPrimaryKeyColumns()))
return false;
if (!match(getPrimaryKeyConstants(), fk.getPrimaryKeyConstants()))
return false;
return true;
}
/**
* Return true if the given local and foreign columns match those
* on this key. This can be used to find foreign keys given only
* column linking information.
*/
public boolean columnsMatch(Column[] fkCols, Column[] fkPKCols) {
return match(getColumns(), fkCols)
&& match(getPrimaryKeyColumns(), fkPKCols);
}
/**
* Checks for non-nullable local columns.
*/
public boolean hasNotNullColumns() {
Column[] columns = getColumns();
for (int j = 0; j < columns.length; j++) {
if (columns[j].isNotNull()) {
return true;
}
}
return false;
}
private static boolean match(Column[] cols, Column[] fkCols) {
if (cols.length != fkCols.length)
return false;
for (int i = 0; i < fkCols.length; i++)
if (!hasColumn(cols, fkCols[i]))
return false;
return true;
}
private static boolean hasColumn(Column[] cols, Column col) {
for (int i = 0; i < cols.length; i++)
if (cols[i].getFullName().equalsIgnoreCase(col.getFullName()))
return true;
return false;
}
private static boolean match(Object[] vals, Object[] fkVals) {
if (vals.length != fkVals.length)
return false;
for (int i = 0; i < vals.length; i++)
if (!ObjectUtils.equals(vals[i], fkVals[i]))
return false;
return true;
}
}