001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: */
019: package org.apache.openjpa.jdbc.schema;
020:
021: import java.sql.DatabaseMetaData;
022: import java.util.ArrayList;
023: import java.util.Arrays;
024: import java.util.List;
025: import java.util.LinkedHashMap;
026:
027: import org.apache.commons.lang.ObjectUtils;
028: import org.apache.openjpa.lib.util.Localizer;
029: import org.apache.openjpa.lib.util.StringDistance;
030: import org.apache.openjpa.util.InvalidStateException;
031:
032: /**
033: * Represents a database foreign key; may be a logical key with no
034: * database representation. This class can also represent a partial key,
035: * aligning with {@link DatabaseMetaData}.
036: *
037: * @author Abe White
038: */
039: public class ForeignKey extends Constraint {
040:
041: /**
042: * Logical foreign key; links columns, but does not perform any action
043: * when the joined primary key columns are modified.
044: */
045: public static final int ACTION_NONE = 1;
046:
047: /**
048: * Throw an exception if joined primary key columns are modified.
049: */
050: public static final int ACTION_RESTRICT = 2;
051:
052: /**
053: * Cascade any modification of the joined primary key columns to
054: * this table. If the joined primary key row is deleted, the row in this
055: * table will also be deleted.
056: */
057: public static final int ACTION_CASCADE = 3;
058:
059: /**
060: * Null the local columns if the joined primary key columns are modified.
061: */
062: public static final int ACTION_NULL = 4;
063:
064: /**
065: * Set the local columns to their default values if the primary key
066: * columns are modified.
067: */
068: public static final int ACTION_DEFAULT = 5;
069:
070: private static final Localizer _loc = Localizer
071: .forPackage(ForeignKey.class);
072:
073: private String _pkTableName = null;
074: private String _pkSchemaName = null;
075: private String _pkColumnName = null;
076: private int _seq = 0;
077:
078: private LinkedHashMap _joins = null;
079: private LinkedHashMap _joinsPK = null;
080: private LinkedHashMap _consts = null;
081: private LinkedHashMap _constsPK = null;
082: private int _delAction = ACTION_NONE;
083: private int _upAction = ACTION_NONE;
084: private int _index = 0;
085:
086: // cached items
087: private Column[] _locals = null;
088: private Column[] _pks = null;
089: private Object[] _constVals = null;
090: private Column[] _constCols = null;
091: private Object[] _constValsPK = null;
092: private Column[] _constColsPK = null;
093: private Table _pkTable = null;
094: private Boolean _autoAssign = null;
095:
096: /**
097: * Return the foreign key action constant for the given action name.
098: */
099: public static int getAction(String name) {
100: if (name == null || "none".equalsIgnoreCase(name))
101: return ACTION_NONE;
102: if ("cascade".equalsIgnoreCase(name))
103: return ACTION_CASCADE;
104: if ("default".equalsIgnoreCase(name))
105: return ACTION_DEFAULT;
106: if ("restrict".equalsIgnoreCase(name)
107: || "exception".equalsIgnoreCase(name))
108: return ACTION_RESTRICT;
109: if ("null".equalsIgnoreCase(name))
110: return ACTION_NULL;
111:
112: // not a recognized action; check for typo
113: List recognized = Arrays
114: .asList(new String[] { "none", "exception", "restrict",
115: "cascade", "null", "default", });
116: String closest = StringDistance.getClosestLevenshteinDistance(
117: name, recognized, .5F);
118:
119: String msg;
120: if (closest != null)
121: msg = _loc.get("bad-fk-action-hint", name, closest,
122: recognized).getMessage();
123: else
124: msg = _loc.get("bad-fk-action", name, recognized)
125: .getMessage();
126: throw new IllegalArgumentException(msg);
127: }
128:
129: /**
130: * Return the foreign key action name for the given action constant.
131: */
132: public static String getActionName(int action) {
133: switch (action) {
134: case ACTION_NONE:
135: return "none";
136: case ACTION_RESTRICT:
137: return "restrict";
138: case ACTION_CASCADE:
139: return "cascade";
140: case ACTION_DEFAULT:
141: return "default";
142: case ACTION_NULL:
143: return "null";
144: default:
145: throw new IllegalArgumentException(String.valueOf(action));
146: }
147: }
148:
149: /**
150: * Default constructor.
151: */
152: public ForeignKey() {
153: }
154:
155: /**
156: * Constructor.
157: *
158: * @param name the foreign key name, if any
159: * @param table the local table of the foreign key
160: */
161: public ForeignKey(String name, Table table) {
162: super (name, table);
163: }
164:
165: public boolean isLogical() {
166: return _delAction == ACTION_NONE;
167: }
168:
169: /**
170: * Whether the primary key columns of this key are auto-incrementing, or
171: * whether they themselves are members of a foreign key who's primary key
172: * is auto-incrementing (recursing to arbitrary depth).
173: */
174: public boolean isPrimaryKeyAutoAssigned() {
175: if (_autoAssign != null)
176: return _autoAssign.booleanValue();
177: return isPrimaryKeyAutoAssigned(new ArrayList(3));
178: }
179:
180: /**
181: * Helper to calculate whether this foreign key depends on auto-assigned
182: * columns. Recurses appropriately if the primary key columns this key
183: * joins to are themselves members of a foreign key that is dependent on
184: * auto-assigned columns. Caches calculated auto-assign value as a side
185: * effect.
186: *
187: * @param seen track seen foreign keys to prevent infinite recursion in
188: * the case of foreign key cycles
189: */
190: private boolean isPrimaryKeyAutoAssigned(List seen) {
191: if (_autoAssign != null)
192: return _autoAssign.booleanValue();
193:
194: Column[] cols = getPrimaryKeyColumns();
195: if (cols.length == 0) {
196: _autoAssign = Boolean.FALSE;
197: return false;
198: }
199:
200: for (int i = 0; i < cols.length; i++) {
201: if (cols[i].isAutoAssigned()) {
202: _autoAssign = Boolean.TRUE;
203: return true;
204: }
205: }
206:
207: ForeignKey[] fks = _pkTable.getForeignKeys();
208: seen.add(this );
209: for (int i = 0; i < cols.length; i++) {
210: for (int j = 0; j < fks.length; j++) {
211: if (!fks[j].containsColumn(cols[i]))
212: continue;
213: if (!seen.contains(fks[j])
214: && fks[j].isPrimaryKeyAutoAssigned(seen)) {
215: _autoAssign = Boolean.TRUE;
216: return true;
217: }
218: }
219: }
220:
221: _autoAssign = Boolean.FALSE;
222: return false;
223: }
224:
225: /**
226: * The name of the primary key table.
227: */
228: public String getPrimaryKeyTableName() {
229: Table table = getPrimaryKeyTable();
230: if (table != null)
231: return table.getName();
232: return _pkTableName;
233: }
234:
235: /**
236: * The name of the primary key table. You can only set the primary
237: * key table name on foreign keys that have not already been joined.
238: */
239: public void setPrimaryKeyTableName(String pkTableName) {
240: if (getPrimaryKeyTable() != null)
241: throw new IllegalStateException();
242: _pkTableName = pkTableName;
243: }
244:
245: /**
246: * The name of the primary key table's schema.
247: */
248: public String getPrimaryKeySchemaName() {
249: Table table = getPrimaryKeyTable();
250: if (table != null)
251: return table.getSchemaName();
252: return _pkSchemaName;
253: }
254:
255: /**
256: * The name of the primary key table's schema. You can only set the
257: * primary key schema name on foreign keys that have not already been
258: * joined.
259: */
260: public void setPrimaryKeySchemaName(String pkSchemaName) {
261: if (getPrimaryKeyTable() != null)
262: throw new IllegalStateException();
263: _pkSchemaName = pkSchemaName;
264: }
265:
266: /**
267: * The name of the primary key column.
268: */
269: public String getPrimaryKeyColumnName() {
270: return _pkColumnName;
271: }
272:
273: /**
274: * The name of the primary key column. You can only set the
275: * primary key column name on foreign keys that have not already been
276: * joined.
277: */
278: public void setPrimaryKeyColumnName(String pkColumnName) {
279: if (getPrimaryKeyTable() != null)
280: throw new IllegalStateException();
281: _pkColumnName = pkColumnName;
282: }
283:
284: /**
285: * The sequence of this join in the foreign key.
286: */
287: public int getKeySequence() {
288: return _seq;
289: }
290:
291: /**
292: * The sequence of this join in the foreign key.
293: */
294: public void setKeySequence(int seq) {
295: _seq = seq;
296: }
297:
298: /**
299: * Return the delete action for the key. Will be one of:
300: * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
301: * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
302: */
303: public int getDeleteAction() {
304: return _delAction;
305: }
306:
307: /**
308: * Set the delete action for the key. Must be one of:
309: * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
310: * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
311: */
312: public void setDeleteAction(int action) {
313: _delAction = action;
314: if (action == ACTION_NONE)
315: _upAction = ACTION_NONE;
316: else if (_upAction == ACTION_NONE)
317: _upAction = ACTION_RESTRICT;
318: }
319:
320: /**
321: * Return the update action for the key. Will be one of:
322: * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
323: * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
324: */
325: public int getUpdateAction() {
326: return _upAction;
327: }
328:
329: /**
330: * Set the update action for the key. Must be one of:
331: * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
332: * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
333: */
334: public void setUpdateAction(int action) {
335: _upAction = action;
336: if (action == ACTION_NONE)
337: _delAction = ACTION_NONE;
338: else if (_delAction == ACTION_NONE)
339: _delAction = ACTION_RESTRICT;
340: }
341:
342: /**
343: * Return the foreign key's 0-based index in the owning table.
344: */
345: public int getIndex() {
346: Table table = getTable();
347: if (table != null)
348: table.indexForeignKeys();
349: return _index;
350: }
351:
352: /**
353: * Set the foreign key's 0-based index in the owning table.
354: */
355: void setIndex(int index) {
356: _index = index;
357: }
358:
359: /**
360: * Return the primary key column joined to the given local column.
361: */
362: public Column getPrimaryKeyColumn(Column local) {
363: return (_joins == null) ? null : (Column) _joins.get(local);
364: }
365:
366: /**
367: * Return the local column joined to the given primary key column.
368: */
369: public Column getColumn(Column pk) {
370: return (_joinsPK == null) ? null : (Column) _joinsPK.get(pk);
371: }
372:
373: /**
374: * Return the constant value assigned to the given local column.
375: */
376: public Object getConstant(Column local) {
377: return (_consts == null) ? null : _consts.get(local);
378: }
379:
380: /**
381: * Return the constant value assigned to the given primary key column.
382: */
383: public Object getPrimaryKeyConstant(Column pk) {
384: return (_constsPK == null) ? null : _constsPK.get(pk);
385: }
386:
387: /**
388: * Return the local columns in the foreign key local table order.
389: */
390: public Column[] getColumns() {
391: if (_locals == null)
392: _locals = (_joins == null) ? Schemas.EMPTY_COLUMNS
393: : (Column[]) _joins.keySet().toArray(
394: new Column[_joins.size()]);
395: return _locals;
396: }
397:
398: /**
399: * Return the constant values assigned to the local columns
400: * returned by {@link #getConstantColumns}.
401: */
402: public Object[] getConstants() {
403: if (_constVals == null)
404: _constVals = (_consts == null) ? Schemas.EMPTY_VALUES
405: : _consts.values().toArray();
406: return _constVals;
407: }
408:
409: /**
410: * Return the constant values assigned to the primary key columns
411: * returned by {@link #getConstantPrimaryKeyColumns}.
412: */
413: public Object[] getPrimaryKeyConstants() {
414: if (_constValsPK == null)
415: _constValsPK = (_constsPK == null) ? Schemas.EMPTY_VALUES
416: : _constsPK.values().toArray();
417: return _constValsPK;
418: }
419:
420: /**
421: * Return true if the fk includes the given local column.
422: */
423: public boolean containsColumn(Column col) {
424: return _joins != null && _joins.containsKey(col);
425: }
426:
427: /**
428: * Return true if the fk includes the given primary key column.
429: */
430: public boolean containsPrimaryKeyColumn(Column col) {
431: return _joinsPK != null && _joinsPK.containsKey(col);
432: }
433:
434: /**
435: * Return true if the fk includes the given local column.
436: */
437: public boolean containsConstantColumn(Column col) {
438: return _consts != null && _consts.containsKey(col);
439: }
440:
441: /**
442: * Return true if the fk includes the given primary key column.
443: */
444: public boolean containsConstantPrimaryKeyColumn(Column col) {
445: return _constsPK != null && _constsPK.containsKey(col);
446: }
447:
448: /**
449: * Return the foreign columns in the foreign key, in join-order with
450: * the result of {@link #getColumns}.
451: */
452: public Column[] getPrimaryKeyColumns() {
453: if (_pks == null)
454: _pks = (_joins == null) ? Schemas.EMPTY_COLUMNS
455: : (Column[]) _joins.values().toArray(
456: new Column[_joins.size()]);
457: return _pks;
458: }
459:
460: /**
461: * Return the local columns that we link to using constant values.
462: */
463: public Column[] getConstantColumns() {
464: if (_constCols == null)
465: _constCols = (_consts == null) ? Schemas.EMPTY_COLUMNS
466: : (Column[]) _consts.keySet().toArray(
467: new Column[_consts.size()]);
468: return _constCols;
469: }
470:
471: /**
472: * Return the primary key columns that we link to using constant values.
473: */
474: public Column[] getConstantPrimaryKeyColumns() {
475: if (_constColsPK == null)
476: _constColsPK = (_constsPK == null) ? Schemas.EMPTY_COLUMNS
477: : (Column[]) _constsPK.keySet().toArray(
478: new Column[_constsPK.size()]);
479: return _constColsPK;
480: }
481:
482: /**
483: * Set the foreign key's joins.
484: */
485: public void setJoins(Column[] cols, Column[] pkCols) {
486: Column[] cur = getColumns();
487: for (int i = 0; i < cur.length; i++)
488: removeJoin(cur[i]);
489:
490: if (cols != null)
491: for (int i = 0; i < cols.length; i++)
492: join(cols[i], pkCols[i]);
493: }
494:
495: /**
496: * Set the foreign key's constant joins.
497: */
498: public void setConstantJoins(Object[] consts, Column[] pkCols) {
499: Column[] cur = getConstantPrimaryKeyColumns();
500: for (int i = 0; i < cur.length; i++)
501: removeJoin(cur[i]);
502:
503: if (consts != null)
504: for (int i = 0; i < consts.length; i++)
505: joinConstant(consts[i], pkCols[i]);
506: }
507:
508: /**
509: * Set the foreign key's constant joins.
510: */
511: public void setConstantJoins(Column[] cols, Object[] consts) {
512: Column[] cur = getConstantColumns();
513: for (int i = 0; i < cur.length; i++)
514: removeJoin(cur[i]);
515:
516: if (consts != null)
517: for (int i = 0; i < consts.length; i++)
518: joinConstant(cols[i], consts[i]);
519: }
520:
521: /**
522: * Join a local column to a primary key column of another table.
523: */
524: public void join(Column local, Column toPK) {
525: if (!ObjectUtils.equals(local.getTable(), getTable()))
526: throw new InvalidStateException(_loc.get("table-mismatch",
527: local.getTable(), getTable()));
528:
529: Table pkTable = toPK.getTable();
530: if (_pkTable != null && !_pkTable.equals(pkTable))
531: throw new InvalidStateException(_loc.get("fk-mismatch",
532: pkTable, _pkTable));
533:
534: _pkTable = pkTable;
535: if (_joins == null)
536: _joins = new LinkedHashMap();
537: _joins.put(local, toPK);
538: if (_joinsPK == null)
539: _joinsPK = new LinkedHashMap();
540: _joinsPK.put(toPK, local);
541:
542: // force re-cache
543: _locals = null;
544: _pks = null;
545: if (_autoAssign == Boolean.FALSE)
546: _autoAssign = null;
547: }
548:
549: /**
550: * Join a constant value to a primary key column of another table. The
551: * constant must be either a string or a number.
552: */
553: public void joinConstant(Object val, Column toPK) {
554: Table pkTable = toPK.getTable();
555: if (_pkTable != null && !_pkTable.equals(pkTable))
556: throw new InvalidStateException(_loc.get("fk-mismatch",
557: pkTable, _pkTable));
558:
559: _pkTable = pkTable;
560: if (_constsPK == null)
561: _constsPK = new LinkedHashMap();
562: _constsPK.put(toPK, val);
563:
564: // force re-cache
565: _constValsPK = null;
566: _constColsPK = null;
567: }
568:
569: /**
570: * Join a constant value to a local column of this table. The
571: * constant must be either a string or a number.
572: */
573: public void joinConstant(Column col, Object val) {
574: if (_consts == null)
575: _consts = new LinkedHashMap();
576: _consts.put(col, val);
577:
578: // force re-cache
579: _constVals = null;
580: _constCols = null;
581: }
582:
583: /**
584: * Remove any joins inolving the given column.
585: *
586: * @return true if the join was removed, false if not part of the key
587: */
588: public boolean removeJoin(Column col) {
589: boolean remd = false;
590: Object rem;
591:
592: if (_joins != null) {
593: rem = _joins.remove(col);
594: if (rem != null) {
595: _locals = null;
596: _pks = null;
597: _joinsPK.remove(rem);
598: remd = true;
599: }
600: }
601:
602: if (_joinsPK != null) {
603: rem = _joinsPK.remove(col);
604: if (rem != null) {
605: _locals = null;
606: _pks = null;
607: _joins.remove(rem);
608: remd = true;
609: }
610: }
611:
612: if (_consts != null) {
613: if (_consts.remove(col) != null) {
614: _constVals = null;
615: _constCols = null;
616: remd = true;
617: }
618: }
619:
620: if (_constsPK != null) {
621: if (_constsPK.containsKey(col)) {
622: _constsPK.remove(col);
623: _constValsPK = null;
624: _constColsPK = null;
625: remd = true;
626: }
627: }
628:
629: if ((_joins == null || _joins.isEmpty())
630: && (_constsPK == null || _constsPK.isEmpty()))
631: _pkTable = null;
632: if (remd && _autoAssign == Boolean.TRUE)
633: _autoAssign = null;
634: return remd;
635: }
636:
637: /**
638: * Returns the table this foreign key is linking to, if it is known yet.
639: */
640: public Table getPrimaryKeyTable() {
641: return _pkTable;
642: }
643:
644: /**
645: * Ref all columns in this key.
646: */
647: public void refColumns() {
648: Column[] cols = getColumns();
649: for (int i = 0; i < cols.length; i++)
650: cols[i].ref();
651: cols = getConstantColumns();
652: for (int i = 0; i < cols.length; i++)
653: cols[i].ref();
654: }
655:
656: /**
657: * Deref all columns in this key.
658: */
659: public void derefColumns() {
660: Column[] cols = getColumns();
661: for (int i = 0; i < cols.length; i++)
662: cols[i].deref();
663: cols = getConstantColumns();
664: for (int i = 0; i < cols.length; i++)
665: cols[i].deref();
666: }
667:
668: /**
669: * Foreign keys are equal if the satisfy the equality constraints of
670: * {@link Constraint} and they have the same local and primary key
671: * columns and action.
672: */
673: public boolean equalsForeignKey(ForeignKey fk) {
674: if (fk == this )
675: return true;
676: if (fk == null)
677: return false;
678:
679: if (getDeleteAction() != fk.getDeleteAction())
680: return false;
681: if (isDeferred() != fk.isDeferred())
682: return false;
683:
684: if (!columnsMatch(fk.getColumns(), fk.getPrimaryKeyColumns()))
685: return false;
686: if (!match(getConstantColumns(), fk.getConstantColumns()))
687: return false;
688: if (!match(getConstants(), fk.getConstants()))
689: return false;
690: if (!match(getConstantPrimaryKeyColumns(), fk
691: .getConstantPrimaryKeyColumns()))
692: return false;
693: if (!match(getPrimaryKeyConstants(), fk
694: .getPrimaryKeyConstants()))
695: return false;
696: return true;
697: }
698:
699: /**
700: * Return true if the given local and foreign columns match those
701: * on this key. This can be used to find foreign keys given only
702: * column linking information.
703: */
704: public boolean columnsMatch(Column[] fkCols, Column[] fkPKCols) {
705: return match(getColumns(), fkCols)
706: && match(getPrimaryKeyColumns(), fkPKCols);
707: }
708:
709: /**
710: * Checks for non-nullable local columns.
711: */
712: public boolean hasNotNullColumns() {
713: Column[] columns = getColumns();
714: for (int j = 0; j < columns.length; j++) {
715: if (columns[j].isNotNull()) {
716: return true;
717: }
718: }
719: return false;
720: }
721:
722: private static boolean match(Column[] cols, Column[] fkCols) {
723: if (cols.length != fkCols.length)
724: return false;
725: for (int i = 0; i < fkCols.length; i++)
726: if (!hasColumn(cols, fkCols[i]))
727: return false;
728: return true;
729: }
730:
731: private static boolean hasColumn(Column[] cols, Column col) {
732: for (int i = 0; i < cols.length; i++)
733: if (cols[i].getFullName().equalsIgnoreCase(
734: col.getFullName()))
735: return true;
736: return false;
737: }
738:
739: private static boolean match(Object[] vals, Object[] fkVals) {
740: if (vals.length != fkVals.length)
741: return false;
742: for (int i = 0; i < vals.length; i++)
743: if (!ObjectUtils.equals(vals[i], fkVals[i]))
744: return false;
745: return true;
746: }
747: }
|