1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.openjpa.jdbc.meta;
20
21 import java.io.Serializable;
22 import java.sql.Types;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.List;
26
27 import org.apache.commons.lang.StringUtils;
28 import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
29 import org.apache.openjpa.jdbc.schema.Column;
30 import org.apache.openjpa.jdbc.schema.ColumnIO;
31 import org.apache.openjpa.jdbc.schema.ForeignKey;
32 import org.apache.openjpa.jdbc.schema.Index;
33 import org.apache.openjpa.jdbc.schema.PrimaryKey;
34 import org.apache.openjpa.jdbc.schema.Schema;
35 import org.apache.openjpa.jdbc.schema.SchemaGroup;
36 import org.apache.openjpa.jdbc.schema.Schemas;
37 import org.apache.openjpa.jdbc.schema.Table;
38 import org.apache.openjpa.jdbc.schema.Unique;
39 import org.apache.openjpa.jdbc.sql.DBDictionary;
40 import org.apache.openjpa.lib.log.Log;
41 import org.apache.openjpa.lib.util.Localizer;
42 import org.apache.openjpa.lib.util.Localizer.Message;
43 import org.apache.openjpa.meta.JavaTypes;
44 import org.apache.openjpa.meta.MetaDataContext;
45 import org.apache.openjpa.util.MetaDataException;
46 import serp.util.Strings;
47
48 /**
49 * Base class storing raw mapping information; defines utility methods for
50 * converting raw mapping information to full mapping to the schema.
51 *
52 * @author Abe White
53 */
54 public abstract class MappingInfo
55 implements Serializable {
56
57 public static final int JOIN_NONE = 0;
58 public static final int JOIN_FORWARD = 1;
59 public static final int JOIN_INVERSE = 2;
60
61 private static final Object NULL = new Object();
62
63 private static final Localizer _loc = Localizer.forPackage
64 (MappingInfo.class);
65
66 private String _strategy = null;
67 private List _cols = null;
68 private Index _idx = null;
69 private Unique _unq = null;
70 private ForeignKey _fk = null;
71 private boolean _canIdx = true;
72 private boolean _canUnq = true;
73 private boolean _canFK = true;
74 private int _join = JOIN_NONE;
75 private ColumnIO _io = null;
76
77 /**
78 * Mapping strategy name.
79 */
80 public String getStrategy() {
81 return _strategy;
82 }
83
84 /**
85 * Mapping strategy name.
86 */
87 public void setStrategy(String strategy) {
88 _strategy = strategy;
89 }
90
91 /**
92 * Raw column data.
93 */
94 public List getColumns() {
95 return (_cols == null) ? Collections.EMPTY_LIST : _cols;
96 }
97
98 /**
99 * Raw column data.
100 */
101 public void setColumns(List cols) {
102 _cols = cols;
103 }
104
105 /**
106 * Raw index.
107 */
108 public Index getIndex() {
109 return _idx;
110 }
111
112 /**
113 * Raw index.
114 */
115 public void setIndex(Index idx) {
116 _idx = idx;
117 }
118
119 /**
120 * The user can mark columns as explicitly non-indexable.
121 */
122 public boolean canIndex() {
123 return _canIdx;
124 }
125
126 /**
127 * The user can mark columns as explicitly non-indexable.
128 */
129 public void setCanIndex(boolean indexable) {
130 _canIdx = indexable;
131 }
132
133 /**
134 * Raw foreign key information.
135 */
136 public ForeignKey getForeignKey() {
137 return _fk;
138 }
139
140 /**
141 * Raw foreign key information.
142 */
143 public void setForeignKey(ForeignKey fk) {
144 _fk = fk;
145 if (fk != null && _join == JOIN_NONE)
146 _join = JOIN_FORWARD;
147 }
148
149 /**
150 * The user can mark columns as explicitly not having a foreign key.
151 */
152 public boolean canForeignKey() {
153 return _canFK;
154 }
155
156 /**
157 * The user can mark columns as explicitly not having a foreign key.
158 */
159 public void setCanForeignKey(boolean fkable) {
160 _canFK = fkable;
161 }
162
163 /**
164 * Raw unique constraint information.
165 */
166 public Unique getUnique() {
167 return _unq;
168 }
169
170 /**
171 * Raw unique constraint information.
172 */
173 public void setUnique(Unique unq) {
174 _unq = unq;
175 }
176
177 /**
178 * The user can mark columns as explicitly not having a unique constraint.
179 */
180 public boolean canUnique() {
181 return _canUnq;
182 }
183
184 /**
185 * The user can mark columns as explicitly not having a unique constraint.
186 */
187 public void setCanUnique(boolean uniquable) {
188 _canUnq = uniquable;
189 }
190
191 /**
192 * I/O for the columns created by the last call to {@link #createColumns},
193 * or for the foreign key created by the last call to
194 * {@link #createForeignKey}. This is also expected to be set correctly
195 * prior to calls to {@link #syncColumns} and {@link #syncForeignKey}.
196 */
197 public ColumnIO getColumnIO() {
198 return _io;
199 }
200
201 /**
202 * I/O for the columns created by the last call to {@link #createColumns},
203 * or for the foreign key created by the last call to
204 * {@link #createForeignKey}. This is also expected to be set correctly
205 * prior to calls to {@link #syncColumns} and {@link #syncForeignKey}.
206 */
207 public void setColumnIO(ColumnIO io) {
208 _io = io;
209 }
210
211 /**
212 * Direction of the join that the columns of this mapping info form. This
213 * is usually automatically set by {@link #createForeignKey}. This flag
214 * is also expected to be set correctly prior to calls to
215 * {@link #syncForeignKey} if the join is inversed.
216 */
217 public int getJoinDirection() {
218 return _join;
219 }
220
221 /**
222 * Direction of the join that the columns of this mapping info form. This
223 * is usually automatically set by {@link #createForeignKey}. This flag
224 * is also expected to be set correctly prior to calls to
225 * {@link #syncForeignKey} if the join is inversed.
226 */
227 public void setJoinDirection(int join) {
228 _join = join;
229 }
230
231 /**
232 * Clear all mapping information.
233 */
234 public void clear() {
235 clear(true);
236 }
237
238 /**
239 * Clear mapping information.
240 *
241 * @param canFlags whether to clear information about whether we
242 * can place indexed, foreign keys, etc on this mapping
243 */
244 protected void clear(boolean canFlags) {
245 _strategy = null;
246 _cols = null;
247 _io = null;
248 _idx = null;
249 _unq = null;
250 _fk = null;
251 _join = JOIN_NONE;
252 if (canFlags) {
253 _canIdx = true;
254 _canFK = true;
255 _canUnq = true;
256 }
257 }
258
259 /**
260 * Copy missing info from the instance to this one.
261 */
262 public void copy(MappingInfo info) {
263 if (_strategy == null)
264 _strategy = info.getStrategy();
265 if (_canIdx && _idx == null) {
266 if (info.getIndex() != null)
267 _idx = info.getIndex();
268 else
269 _canIdx = info.canIndex();
270 }
271 if (_canUnq && _unq == null) {
272 if (info.getUnique() != null)
273 _unq = info.getUnique();
274 else
275 _canUnq = info.canUnique();
276 }
277 if (_canFK && _fk == null) {
278 if (info.getForeignKey() != null)
279 _fk = info.getForeignKey();
280 else
281 _canFK = info.canForeignKey();
282 }
283
284 List cols = getColumns();
285 List icols = info.getColumns();
286 if (!icols.isEmpty() && (cols.isEmpty()
287 || cols.size() == icols.size())) {
288 if (cols.isEmpty())
289 cols = new ArrayList(icols.size());
290 for (int i = 0; i < icols.size(); i++) {
291 if (cols.size() == i)
292 cols.add(new Column());
293 ((Column) cols.get(i)).copy((Column) icols.get(i));
294 }
295 setColumns(cols);
296 }
297 }
298
299 /**
300 * Return true if this info has columns, foreign key information, index
301 * information, etc.
302 */
303 public boolean hasSchemaComponents() {
304 return (_cols != null && !_cols.isEmpty())
305 || _idx != null
306 || _unq != null
307 || _fk != null
308 || !_canIdx
309 || !_canFK
310 || !_canUnq;
311 }
312
313 /**
314 * Assert that the user did not supply any columns, index, unique
315 * constraint, or foreign key for this mapping.
316 */
317 public void assertNoSchemaComponents(MetaDataContext context, boolean die) {
318 if (_cols == null || _cols.isEmpty()) {
319 assertNoIndex(context, die);
320 assertNoUnique(context, die);
321 assertNoForeignKey(context, die);
322 return;
323 }
324
325 Message msg = _loc.get("unexpected-cols", context);
326 if (die)
327 throw new MetaDataException(msg);
328 context.getRepository().getLog().warn(msg);
329 }
330
331 /**
332 * Assert that this info has the given strategy or no strategy.
333 */
334 public void assertStrategy(MetaDataContext context, Object contextStrat,
335 Object expected, boolean die) {
336 if (contextStrat == expected)
337 return;
338
339 String strat;
340 if (contextStrat == null) {
341 if (_strategy == null)
342 return;
343 if (_strategy.equals(expected.getClass().getName()))
344 return;
345 if (expected instanceof Strategy
346 && _strategy.equals(((Strategy) expected).getAlias()))
347 return;
348 strat = _strategy;
349 } else if (contextStrat instanceof Strategy)
350 strat = ((Strategy) contextStrat).getAlias();
351 else
352 strat = contextStrat.getClass().getName();
353
354 Message msg = _loc.get("unexpected-strategy", context, expected,
355 strat);
356 if (die)
357 throw new MetaDataException(msg);
358 context.getRepository().getLog().warn(msg);
359 }
360
361 /**
362 * Assert that the user did not try to place an index on this mapping.
363 */
364 public void assertNoIndex(MetaDataContext context, boolean die) {
365 if (_idx == null)
366 return;
367
368 Message msg = _loc.get("unexpected-index", context);
369 if (die)
370 throw new MetaDataException(msg);
371 context.getRepository().getLog().warn(msg);
372 }
373
374 /**
375 * Assert that the user did not try to place a unique constraint on this
376 * mapping.
377 */
378 public void assertNoUnique(MetaDataContext context, boolean die) {
379 if (_unq == null)
380 return;
381
382 Message msg = _loc.get("unexpected-unique", context);
383 if (die)
384 throw new MetaDataException(msg);
385 context.getRepository().getLog().warn(msg);
386 }
387
388 /**
389 * Assert that the user did not try to place a foreign key on this mapping.
390 */
391 public void assertNoForeignKey(MetaDataContext context, boolean die) {
392 if (_fk == null)
393 return;
394
395 Message msg = _loc.get("unexpected-fk", context);
396 if (die)
397 throw new MetaDataException(msg);
398 context.getRepository().getLog().warn(msg);
399 }
400
401 /**
402 * Assert that the user did not try to join.
403 */
404 public void assertNoJoin(MetaDataContext context, boolean die) {
405 boolean join = false;
406 if (_cols != null) {
407 Column col;
408 for (int i = 0; !join && i < _cols.size(); i++) {
409 col = (Column) _cols.get(i);
410 if (col.getTarget() != null)
411 join = true;
412 }
413 }
414 if (!join)
415 return;
416
417 Message msg = _loc.get("unexpected-join", context);
418 if (die)
419 throw new MetaDataException(msg);
420 context.getRepository().getLog().warn(msg);
421 }
422
423 /**
424 * Find or generate a table for a mapping.
425 *
426 * @param context the mapping that uses the table
427 * @param def default table name provider
428 * @param schemaName default schema if known, or null
429 * @param given given table name
430 * @param adapt whether we can alter the schema or mappings
431 */
432 public Table createTable(MetaDataContext context, TableDefaults def,
433 String schemaName, String given, boolean adapt) {
434 MappingRepository repos = (MappingRepository) context.getRepository();
435 if (given == null && (def == null || (!adapt
436 && !repos.getMappingDefaults().defaultMissingInfo())))
437 throw new MetaDataException(_loc.get("no-table", context));
438
439 if (schemaName == null)
440 schemaName = Schemas.getNewTableSchema((JDBCConfiguration)
441 repos.getConfiguration());
442
443 // if no given and adapting or defaulting missing info, use template
444 SchemaGroup group = repos.getSchemaGroup();
445 Schema schema = null;
446 if (given == null) {
447 schema = group.getSchema(schemaName);
448 if (schema == null)
449 schema = group.addSchema(schemaName);
450 given = def.get(schema);
451 }
452
453 String fullName;
454 int dotIdx = given.lastIndexOf('.');
455 if (dotIdx == -1)
456 fullName = (schemaName == null) ? given : schemaName + "." + given;
457 else {
458 fullName = given;
459 schema = null;
460 schemaName = given.substring(0, dotIdx);
461 given = given.substring(dotIdx + 1);
462 }
463
464 // look for named table using full name and findTable, which allows
465 // the dynamic schema factory to create the table if needed
466 Table table = group.findTable(fullName);
467 if (table != null)
468 return table;
469 if (!adapt)
470 throw new MetaDataException(_loc.get("bad-table", given, context));
471
472 // named table doesn't exist; create it
473 if (schema == null) {
474 schema = group.getSchema(schemaName);
475 if (schema == null)
476 schema = group.addSchema(schemaName);
477 }
478 table = schema.getTable(given);
479 if (table == null)
480 table = schema.addTable(given);
481 return table;
482 }
483
484 /**
485 * Retrieve/create columns on the given table by merging the given
486 * template information with any user-provided information.
487 *
488 * @param context the mapping we're retrieving columns for
489 * @param prefix localized error message key prefix
490 * @param tmplates template columns
491 * @param table the table for the columns
492 * @param adapt whether we can modify the existing mapping or schema
493 */
494 protected Column[] createColumns(MetaDataContext context, String prefix,
495 Column[] tmplates, Table table, boolean adapt) {
496 assertTable(context, table);
497 if (prefix == null)
498 prefix = "generic";
499
500 // the user has to give the right number of expected columns for this
501 // mapping, or none at all if we're adapting. can't just given one of
502 // n columns because we don't know which of the n columns the info
503 // applies to
504 List given = getColumns();
505 boolean fill = ((MappingRepository) context.getRepository()).
506 getMappingDefaults().defaultMissingInfo();
507 if ((!given.isEmpty() || (!adapt && !fill))
508 && given.size() != tmplates.length)
509 throw new MetaDataException(_loc.get(prefix + "-num-cols",
510 context, String.valueOf(tmplates.length),
511 String.valueOf(given.size())));
512
513 Column[] cols = new Column[tmplates.length];
514 _io = null;
515 Column col;
516 for (int i = 0; i < tmplates.length; i++) {
517 col = (given.isEmpty()) ? null : (Column) given.get(i);
518 cols[i] = mergeColumn(context, prefix, tmplates[i], true, col,
519 table, adapt, fill);
520 setIOFromColumnFlags(col, i);
521 }
522 return cols;
523 }
524
525 /**
526 * Set the proper internal column I/O metadata for the given column's flags.
527 */
528 private void setIOFromColumnFlags(Column col, int i) {
529 if (col == null || (!col.getFlag(Column.FLAG_UNINSERTABLE)
530 && !col.getFlag(Column.FLAG_UNUPDATABLE)))
531 return;
532
533 if (_io == null)
534 _io = new ColumnIO();
535 _io.setInsertable(i, !col.getFlag(Column.FLAG_UNINSERTABLE));
536 _io.setUpdatable(i, !col.getFlag(Column.FLAG_UNUPDATABLE));
537 }
538
539 /**
540 * Assert that the given table is non-null.
541 */
542 private static void assertTable(MetaDataContext context, Table table) {
543 if (table == null)
544 throw new MetaDataException(_loc.get("unmapped", context));
545 }
546
547 /**
548 * Merge the given columns if possible.
549 *
550 * @param context the mapping we're retrieving columns for
551 * @param prefix localized error message key prefix
552 * @param tmplate template for expected column information
553 * @param compat whether the existing column type must be compatible
554 * with the type of the template column
555 * @param given the given column information from mapping info
556 * @param table the table for the columns
557 * @param adapt whether we can modify the existing mapping or schema
558 * @param fill whether to default missing column information
559 */
560 protected static Column mergeColumn(MetaDataContext context, String prefix,
561 Column tmplate, boolean compat, Column given, Table table,
562 boolean adapt, boolean fill) {
563 assertTable(context, table);
564
565 // if not adapting must provide column name at a minimum
566 String colName = (given == null) ? null : given.getName();
567 if (colName == null && !adapt && !fill)
568 throw new MetaDataException(_loc.get(prefix + "-no-col-name",
569 context));
570
571 // determine the column name based on given info, or template if none;
572 // also make sure that if the user gave a column name, he didn't try
573 // to put the column in an unexpected table
574 if (colName == null)
575 colName = tmplate.getName();
576 int dotIdx = colName.lastIndexOf('.');
577 if (dotIdx == 0)
578 colName = colName.substring(1);
579 else if (dotIdx != -1) {
580 findTable(context, colName.substring(0, dotIdx), table,
581 null, null);
582 colName = colName.substring(dotIdx + 1);
583 }
584
585 // find existing column
586 Column col = table.getColumn(colName);
587 if (col == null && !adapt)
588 throw new MetaDataException(_loc.get(prefix + "-bad-col-name",
589 context, colName, table));
590
591 MappingRepository repos = (MappingRepository) context.getRepository();
592 DBDictionary dict = repos.getDBDictionary();
593
594 // use information from template column by default, allowing any
595 // user-given specifics to override it
596 int type = tmplate.getType();
597 int size = tmplate.getSize();
598 if (type == Types.OTHER)
599 type = dict.getJDBCType(tmplate.getJavaType(), size == -1);
600 boolean ttype = true;
601 int otype = type;
602 String typeName = tmplate.getTypeName();
603 Boolean notNull = null;
604 if (tmplate.isNotNullExplicit())
605 notNull = (tmplate.isNotNull()) ? Boolean.TRUE : Boolean.FALSE;
606 int decimals = tmplate.getDecimalDigits();
607 String defStr = tmplate.getDefaultString();
608 boolean autoAssign = tmplate.isAutoAssigned();
609 boolean relationId = tmplate.isRelationId();
610 String targetField = tmplate.getTargetField();
611 if (given != null) {
612 // use given type if provided, but warn if it isn't compatible with
613 // the expected column type
614 if (given.getType() != Types.OTHER) {
615 ttype = false;
616 if (compat && !given.isCompatible(type, typeName, size,
617 decimals)) {
618 Log log = repos.getLog();
619 if (log.isWarnEnabled())
620 log.warn(_loc.get(prefix + "-incompat-col",
621 context, colName, Schemas.getJDBCName(type)));
622 }
623 otype = given.getType();
624 type = dict.getPreferredType(otype);
625 }
626 typeName = given.getTypeName();
627 size = given.getSize();
628 decimals = given.getDecimalDigits();
629
630 // leave this info as the template defaults unless the user
631 // explicitly turns it on in the given column
632 if (given.isNotNullExplicit())
633 notNull = (given.isNotNull()) ? Boolean.TRUE : Boolean.FALSE;
634 if (given.getDefaultString() != null)
635 defStr = given.getDefaultString();
636 if (given.isAutoAssigned())
637 autoAssign = true;
638 if (given.isRelationId())
639 relationId = true;
640 }
641
642 // default char column size if original type is char (test original
643 // type rather than final type because orig might be clob, translated
644 // to an unsized varchar, which is supported by some dbs)
645 if (size == 0 && (otype == Types.VARCHAR || otype == Types.CHAR))
646 size = dict.characterColumnSize;
647
648 // create column, or make sure existing column matches expected type
649 if (col == null) {
650 col = table.addColumn(colName);
651 col.setType(type);
652 } else if ((compat || !ttype) && !col.isCompatible(type, typeName,
653 size, decimals)) {
654 // if existing column isn't compatible with desired type, die if
655 // can't adapt, else warn and change the existing column type
656 Message msg = _loc.get(prefix + "-bad-col", context,
657 Schemas.getJDBCName(type), col.getDescription());
658 if (!adapt)
659 throw new MetaDataException(msg);
660 Log log = repos.getLog();
661 if (log.isWarnEnabled())
662 log.warn(msg);
663
664 col.setType(type);
665 } else if (given != null && given.getType() != Types.OTHER) {
666 // as long as types are compatible, set column to expected type
667 col.setType(type);
668 }
669
670 // always set the java type and autoassign to expected values, even on
671 // an existing column, since we don't get this from the DB
672 if (compat)
673 col.setJavaType(tmplate.getJavaType());
674 else if (col.getJavaType() == JavaTypes.OBJECT) {
675 if (given != null && given.getJavaType() != JavaTypes.OBJECT)
676 col.setJavaType(given.getJavaType());
677 else
678 col.setJavaType(JavaTypes.getTypeCode
679 (Schemas.getJavaType(col.getType(), col.getSize(),
680 col.getDecimalDigits())));
681 }
682 col.setAutoAssigned(autoAssign);
683 col.setRelationId(relationId);
684 col.setTargetField(targetField);
685
686 // we need this for runtime, and the dynamic schema factory might
687 // not know it, so set it even if not adapting
688 if (defStr != null)
689 col.setDefaultString(defStr);
690 if (notNull != null)
691 col.setNotNull(notNull.booleanValue());
692
693 // add other details if adapting
694 if (adapt) {
695 if (typeName != null)
696 col.setTypeName(typeName);
697 if (size != 0)
698 col.setSize(size);
699 if (decimals != 0)
700 col.setDecimalDigits(decimals);
701 }
702
703 if (tmplate.hasComment())
704 col.setComment(tmplate.getComment());
705 return col;
706 }
707
708 /**
709 * Find the table named by a column or target.
710 *
711 * @param context context for error messages, etc.
712 * @param name the table name, possibly including schema
713 * @param expected the expected table; may be null
714 * @param inverse the possible inverse table; may be null
715 * @param rel if we're finding the target table of a join, the
716 * joined-to type; allows us to also look in its superclass tables
717 */
718 private static Table findTable(MetaDataContext context, String name,
719 Table expected, Table inverse, ClassMapping rel) {
720 // is this the expected table?
721 if (expected == null && rel != null)
722 expected = rel.getTable();
723 if (expected != null && isTableName(name, expected))
724 return expected;
725
726 // check for inverse
727 if (inverse != null && isTableName(name, inverse))
728 return inverse;
729
730 // superclass table?
731 if (rel != null)
732 rel = rel.getJoinablePCSuperclassMapping();
733 while (rel != null) {
734 if (isTableName(name, rel.getTable()))
735 return rel.getTable();
736 rel = rel.getJoinablePCSuperclassMapping();
737 }
738
739 // none of the possible tables
740 throw new MetaDataException(_loc.get("col-wrong-table", context,
741 expected, name));
742 }
743
744 /**
745 * Return whether the given name matches the given table.
746 */
747 private static boolean isTableName(String name, Table table) {
748 return name.equalsIgnoreCase(table.getName())
749 || name.equalsIgnoreCase(table.getFullName());
750 }
751
752 /**
753 * Retrieve/create an index on the given columns by merging the given
754 * template information with any user-provided information.
755 *
756 * @param context the mapping we're retrieving an index for
757 * @param prefix localized error message key prefix
758 * @param tmplate template for expected index information
759 * @param cols the indexed columns
760 * @param adapt whether we can modify the existing mapping or schema
761 */
762 protected Index createIndex(MetaDataContext context, String prefix,
763 Index tmplate, Column[] cols, boolean adapt) {
764 if (prefix == null)
765 prefix = "generic";
766
767 // can't create an index if there are no cols
768 if (cols == null || cols.length == 0) {
769 if (_idx != null)
770 throw new MetaDataException(_loc.get(prefix
771 + "-no-index-cols", context));
772 return null;
773 }
774
775 // look for an existing index on these columns
776 Table table = cols[0].getTable();
777 Index[] idxs = table.getIndexes();
778 Index exist = null;
779 for (int i = 0; i < idxs.length; i++) {
780 if (idxs[i].columnsMatch(cols)) {
781 exist = idxs[i];
782 break;
783 }
784 }
785
786 // remove existing index?
787 if (!_canIdx) {
788 if (exist == null)
789 return null;
790 if (!adapt)
791 throw new MetaDataException(_loc.get(prefix + "-index-exists",
792 context));
793 table.removeIndex(exist);
794 return null;
795 }
796
797 // if we have an existing index, merge given info into it
798 if (exist != null) {
799 if (_idx != null && _idx.isUnique() && !exist.isUnique()) {
800 if (!adapt)
801 throw new MetaDataException(_loc.get(prefix
802 + "-index-not-unique", context));
803 exist.setUnique(true);
804 }
805 return exist;
806 }
807
808 // if no defaults return null
809 MappingRepository repos = (MappingRepository) context.getRepository();
810 boolean fill = repos.getMappingDefaults().defaultMissingInfo();
811 if (_idx == null && (tmplate == null || (!adapt && !fill)))
812 return null;
813
814 String name = null;
815 boolean unq;
816 if (_idx != null) {
817 name = _idx.getName();
818 unq = _idx.isUnique();
819 // preserve multiple columns if they are specified in the index
820 if (_idx.getColumns() != null && _idx.getColumns().length > 1)
821 cols = _idx.getColumns();
822 } else
823 unq = tmplate.isUnique();
824
825 // if no name provided by user info, make one
826 if (name == null) {
827 if (tmplate != null)
828 name = tmplate.getName();
829 else {
830 name = cols[0].getName();
831 name = repos.getDBDictionary().getValidIndexName(name, table);
832 }
833 }
834
835 Index idx = table.addIndex(name);
836 idx.setUnique(unq);
837 idx.setColumns(cols);
838 return idx;
839 }
840
841 /**
842 * Retrieve/create a unique constraint on the given columns by merging the
843 * given template information with any user-provided information.
844 *
845 * @param context the mapping we're retrieving a constraint for
846 * @param prefix localized error message key prefix
847 * @param tmplate template for expected unique information
848 * @param cols the constraint columns
849 * @param adapt whether we can modify the existing mapping or schema
850 */
851 protected Unique createUnique(MetaDataContext context, String prefix,
852 Unique tmplate, Column[] cols, boolean adapt) {
853 if (prefix == null)
854 prefix = "generic";
855
856 // can't create a constraint if there are no cols
857 if (cols == null || cols.length == 0) {
858 if (_unq != null || tmplate != null)
859 throw new MetaDataException(_loc.get(prefix
860 + "-no-unique-cols", context));
861 return null;
862 }
863
864 // look for an existing constraint on these columns
865 Table table = cols[0].getTable();
866 Unique[] unqs = table.getUniques();
867 Unique exist = null;
868 for (int i = 0; i < unqs.length; i++) {
869 if (unqs[i].columnsMatch(cols)) {
870 exist = unqs[i];
871 break;
872 }
873 }
874
875 // remove existing unique?
876 if (!_canUnq) {
877 if (exist == null)
878 return null;
879 if (!adapt)
880 throw new MetaDataException(_loc.get(prefix
881 + "-unique-exists", context));
882 table.removeUnique(exist);
883 return null;
884 }
885
886 // no defaults; return existing constraint (if any)
887 if (tmplate == null && _unq == null)
888 return exist;
889
890 MappingRepository repos = (MappingRepository) context.getRepository();
891 if (exist != null) {
892 if (_unq != null && _unq.isDeferred() && !exist.isDeferred()) {
893 Log log = repos.getLog();
894 if (log.isWarnEnabled())
895 log.warn(_loc.get(prefix + "-defer-unique", context));
896 }
897 return exist;
898 }
899
900 // dict can't handle unique constraints?
901 DBDictionary dict = repos.getDBDictionary();
902 if (_unq != null && !dict.supportsUniqueConstraints) {
903 Log log = repos.getLog();
904 if (log.isWarnEnabled())
905 log.warn(_loc.get(prefix + "-unique-support", context));
906 return null;
907 }
908
909 boolean fill = repos.getMappingDefaults().defaultMissingInfo();
910 if (!adapt && !fill && _unq == null)
911 return null;
912
913 String name;
914 boolean deferred;
915 if (_unq != null) {
916 name = _unq.getName();
917 deferred = _unq.isDeferred();
918 } else {
919 name = tmplate.getName();
920 deferred = tmplate.isDeferred();
921 }
922
923 if (deferred && !dict.supportsDeferredConstraints) {
924 Log log = repos.getLog();
925 if (log.isWarnEnabled())
926 log.warn(_loc.get(prefix + "-create-defer-unique",
927 context, dict.platform));
928 deferred = false;
929 }
930
931 Unique unq = table.addUnique(name);
932 unq.setDeferred(deferred);
933 unq.setColumns(cols);
934 return unq;
935 }
936
937 /**
938 * Retrieve/create a foreign key (possibly logical) on the given columns
939 * by merging the given template information with any user-provided
940 * information.
941 *
942 * @param context the mapping we're retrieving a key for
943 * @param prefix localized error message key prefix
944 * @param given the columns given by the user
945 * @param def defaults provider
946 * @param table the table for the key
947 * @param cls type we're joining from
948 * @param rel target type we're joining to
949 * @param inversable whether the foreign key can be inversed
950 * @param adapt whether we can modify the existing mapping or schema
951 */
952 protected ForeignKey createForeignKey(MetaDataContext context,
953 String prefix, List given, ForeignKeyDefaults def, Table table,
954 ClassMapping cls, ClassMapping rel, boolean inversable, boolean adapt) {
955 assertTable(context, table);
956 if (prefix == null)
957 prefix = "generic";
958
959 // collect the foreign key columns and their targets
960 Object[][] joins = createJoins(context, prefix, table, cls, rel,
961 given, def, inversable, adapt);
962 _join = JOIN_FORWARD;
963
964 // establish local table using any join between two columns; if we only
965 // find constant joins, then keep default local table (directionless)
966 Table local = table;
967 Table foreign = rel.getTable();
968 Table tmp;
969 boolean constant = false;
970 boolean localSet = false;
971 for (int i = 0; i < joins.length; i++) {
972 if (joins[i][1]instanceof Column) {
973 tmp = ((Column) joins[i][0]).getTable();
974 if (!localSet) {
975 local = tmp;
976 localSet = true;
977 } else if (tmp != local)
978 throw new MetaDataException(_loc.get(prefix
979 + "-mult-fk-tables", context, local, tmp));
980 foreign = ((Column) joins[i][1]).getTable();
981
982 if (joins[i][2] == Boolean.TRUE)
983 _join = JOIN_INVERSE;
984 } else
985 constant = true;
986 }
987
988 // if this is not a constant join, look for existing foreign key
989 // on local columns
990 ForeignKey exist = null;
991 if (!constant && local.getForeignKeys().length > 0) {
992 Column[] cols = new Column[joins.length];
993 Column[] pks = new Column[joins.length];
994 for (int i = 0; i < joins.length; i++) {
995 cols[i] = (Column) joins[i][0];
996 pks[i] = (Column) joins[i][1];
997 }
998
999 ForeignKey[] fks = local.getForeignKeys();
1000 for (int i = 0; i < fks.length; i++) {
1001 if (fks[i].getConstantColumns().length == 0
1002 && fks[i].getConstantPrimaryKeyColumns().length == 0
1003 && fks[i].columnsMatch(cols, pks)) {
1004 exist = fks[i];
1005 break;
1006 }
1007 }
1008 }
1009
1010 MappingRepository repos = (MappingRepository) context.getRepository();
1011 DBDictionary dict = repos.getDBDictionary();
1012 if (exist != null) {
1013 // make existing key logical?
1014 if (!_canFK) {
1015 if (exist.getDeleteAction() != exist.ACTION_NONE && !adapt)
1016 throw new MetaDataException(_loc.get(prefix
1017 + "-fk-exists", context));
1018 exist.setDeleteAction(exist.ACTION_NONE);
1019 }
1020
1021 if (_fk != null && _fk.isDeferred() && !exist.isDeferred()) {
1022 Log log = repos.getLog();
1023 if (log.isWarnEnabled())
1024 log.warn(_loc.get(prefix + "-defer-fk", context));
1025 }
1026
1027 // allow user-given info to override existing key if we're adapting;
1028 // template info cannot override existing key
1029 if (adapt && _fk != null) {
1030 if (_fk.getUpdateAction() != ForeignKey.ACTION_NONE)
1031 exist.setUpdateAction(_fk.getUpdateAction());
1032 if (_fk.getDeleteAction() != ForeignKey.ACTION_NONE)
1033 exist.setDeleteAction(_fk.getDeleteAction());
1034 }
1035 setIOFromJoins(exist, joins);
1036 return exist;
1037 }
1038
1039 String name = null;
1040 int delAction = ForeignKey.ACTION_NONE;
1041 int upAction = ForeignKey.ACTION_NONE;
1042 boolean deferred = false;
1043 boolean fill = repos.getMappingDefaults().defaultMissingInfo();
1044 ForeignKey tmplate = (def == null) ? null
1045 : def.get(local, foreign, _join == JOIN_INVERSE);
1046 if (_fk != null && (tmplate == null || (!adapt && !fill))) {
1047 // if not adapting or no template info use given data
1048 name = _fk.getName();
1049 delAction = _fk.getDeleteAction();
1050 upAction = _fk.getUpdateAction();
1051 deferred = _fk.isDeferred();
1052 } else if (_canFK && (adapt || fill)) {
1053 if (_fk == null && tmplate != null) {
1054 // no user given info; use template data
1055 name = tmplate.getName();
1056 delAction = tmplate.getDeleteAction();
1057 upAction = tmplate.getUpdateAction();
1058 deferred = tmplate.isDeferred();
1059 } else if (_fk != null && tmplate != null) {
1060 // merge user and template data, always letting user info win
1061 name = _fk.getName();
1062 if (name == null && tmplate.getName() != null)
1063 name = tmplate.getName();
1064 delAction = _fk.getDeleteAction();
1065 if (delAction == ForeignKey.ACTION_NONE)
1066 delAction = tmplate.getDeleteAction();
1067 upAction = _fk.getUpdateAction();
1068 if (upAction == ForeignKey.ACTION_NONE)
1069 upAction = tmplate.getUpdateAction();
1070 deferred = _fk.isDeferred();
1071 }
1072 }
1073
1074 if (!dict.supportsDeleteAction(delAction)
1075 || !dict.supportsUpdateAction(upAction)) {
1076 Log log = repos.getLog();
1077 if (log.isWarnEnabled())
1078 log.warn(_loc.get(prefix + "-unsupported-fk-action", context));
1079 delAction = ForeignKey.ACTION_NONE;
1080 upAction = ForeignKey.ACTION_NONE;
1081 }
1082 if (deferred && !dict.supportsDeferredConstraints) {
1083 Log log = repos.getLog();
1084 if (log.isWarnEnabled())
1085 log.warn(_loc.get(prefix + "-create-defer-fk",
1086 context, dict.platform));
1087 deferred = false;
1088 }
1089
1090 // create foreign key with merged info
1091 ForeignKey fk = local.addForeignKey(name);
1092 fk.setDeleteAction(delAction);
1093 fk.setUpdateAction(upAction);
1094 fk.setDeferred(deferred);
1095
1096 // add joins to key
1097 Column col;
1098 for (int i = 0; i < joins.length; i++) {
1099 col = (Column) joins[i][0];
1100 if (joins[i][1]instanceof Column)
1101 fk.join(col, (Column) joins[i][1]);
1102 else if ((joins[i][2] == Boolean.TRUE) != (_join == JOIN_INVERSE))
1103 fk.joinConstant(joins[i][1], col);
1104 else
1105 fk.joinConstant(col, joins[i][1]);
1106 }
1107 setIOFromJoins(fk, joins);
1108 return fk;
1109 }
1110
1111 /**
1112 * Use the join information to populate our internal column I/O data.
1113 */
1114 private void setIOFromJoins(ForeignKey fk, Object[][] joins) {
1115 List cols = getColumns();
1116 _io = null;
1117 if (cols.isEmpty())
1118 return;
1119
1120 int constIdx = 0;
1121 int idx;
1122 for (int i = 0; i < joins.length; i++) {
1123 // const columns are indexed after std join columns in fk IO
1124 if (joins[i][1]instanceof Column)
1125 idx = i - constIdx;
1126 else if ((joins[i][2] == Boolean.TRUE) == (_join == JOIN_INVERSE))
1127 idx = fk.getColumns().length + constIdx++;
1128 else
1129 continue;
1130 setIOFromColumnFlags((Column) cols.get(i), idx);
1131 }
1132 }
1133
1134 /**
1135 * Create or retrieve the foreign key joins.
1136 *
1137 * @param context the mapping we're retrieving a key for
1138 * @param prefix localized error message key prefix
1139 * @param table the table for the key
1140 * @param cls type we're joining from, if applicable
1141 * @param rel target type we're joining to
1142 * @param given the columns given by the user
1143 * @param def foreign key defaults provider
1144 * @param inversable whether the foreign key can be inversed
1145 * @param adapt whether we can modify the existing mapping or schema
1146 * @return array of tuples where the first element is the
1147 * local column (or in the case of a constant join the
1148 * sole column), the second is the target column (or
1149 * constant), and the third is {@link Boolean#TRUE} if
1150 * this is an inverse join
1151 */
1152 private Object[][] createJoins(MetaDataContext context,
1153 String prefix, Table table, ClassMapping cls, ClassMapping rel,
1154 List given, ForeignKeyDefaults def, boolean inversable, boolean adapt) {
1155 MappingRepository repos = (MappingRepository) context.getRepository();
1156 boolean fill = repos.getMappingDefaults().defaultMissingInfo();
1157 Object[][] joins;
1158
1159 // if no columns given, just create mirrors of target columns
1160 if (given.isEmpty()) {
1161 if (!adapt && !fill)
1162 throw new MetaDataException(_loc.get(prefix + "-no-fk-cols",
1163 context));
1164
1165 Column[] targets = rel.getPrimaryKeyColumns();
1166 joins = new Object[targets.length][3];
1167 Column tmplate;
1168 for (int i = 0; i < targets.length; i++) {
1169 tmplate = new Column();
1170 tmplate.setName(targets[i].getName());
1171 tmplate.setJavaType(targets[i].getJavaType());
1172 tmplate.setType(targets[i].getType());
1173 tmplate.setTypeName(targets[i].getTypeName());
1174 tmplate.setSize(targets[i].getSize());
1175 tmplate.setDecimalDigits(targets[i].getDecimalDigits());
1176
1177 if (def != null)
1178 def.populate(table, rel.getTable(), tmplate, targets[i],
1179 false, i, targets.length);
1180 joins[i][0] = mergeColumn(context, prefix, tmplate, true,
1181 null, table, adapt, fill);
1182 joins[i][1] = targets[i];
1183 }
1184 return joins;
1185 }
1186
1187 // use given columns to create join. we don't try to use any of the
1188 // template columns, even if the user doesn't give a column linking to
1189 // every primary key of the target type -- users are allowed to create
1190 // partial joins. this means, though, that if a user wants to specify
1191 // info for one join column, he has to at least create elements for
1192 // all of them
1193
1194 joins = new Object[given.size()][3];
1195 Column col;
1196 for (int i = 0; i < joins.length; i++) {
1197 col = (Column) given.get(i);
1198 mergeJoinColumn(context, prefix, col, joins, i, table, cls, rel,
1199 def, inversable && !col.getFlag(Column.FLAG_PK_JOIN), adapt,
1200 fill);
1201 }
1202 return joins;
1203 }
1204
1205 /**
1206 * Create or retrieve a foreign key column for a join.
1207 *
1208 * @param context the mapping we're retrieving a key for
1209 * @param prefix localized error message key prefix
1210 * @param given the given local foreign key column
1211 * @param joins array of joins
1212 * @param idx index of the join array to populate
1213 * @param table the table for the key
1214 * @param cls the type we're joining from
1215 * @param rel target type we're joining to
1216 * @param def foreign key defaults provider;
1217 * use null to mirror target column names
1218 * @param inversable whether the foreign key can be inversed
1219 * @param adapt whether we can modify the existing mapping or schema
1220 * @param fill whether to default missing column information
1221 */
1222 private void mergeJoinColumn(MetaDataContext context, String prefix,
1223 Column given, Object[][] joins, int idx, Table table, ClassMapping cls,
1224 ClassMapping rel, ForeignKeyDefaults def, boolean inversable,
1225 boolean adapt, boolean fill) {
1226 // default to the primary key column name if this is a pk join
1227 String name = given.getName();
1228 if (name == null && given != null
1229 && given.getFlag(Column.FLAG_PK_JOIN) && cls != null) {
1230 Column[] pks = cls.getPrimaryKeyColumns();
1231 if (pks.length == 1)
1232 name = pks[0].getName();
1233 }
1234
1235 // if we can't adapt, then the user must at least give a column name
1236 if (name == null && !adapt && !fill)
1237 throw new MetaDataException(_loc.get(prefix + "-no-fkcol-name",
1238 context));
1239
1240 // check to see if the column isn't in the expected table; it might
1241 // be an inverse join or a join to a base class of the target type
1242 Table local = table;
1243 Table foreign = rel.getTable();
1244 boolean fullName = false;
1245 boolean inverse = false;
1246 if (name != null) {
1247 int dotIdx = name.lastIndexOf('.');
1248 if (dotIdx != -1) {
1249 // allow use of '.' without prefix to mean "use expected
1250 // foreign table"
1251 if (dotIdx == 0)
1252 local = foreign;
1253 else
1254 local = findTable(context, name.substring(0, dotIdx),
1255 local, foreign, null);
1256 fullName = true;
1257 name = name.substring(dotIdx + 1);
1258
1259 // if inverse join, then swap local and foreign tables
1260 if (local != table) {
1261 foreign = table;
1262 inverse = true;
1263 }
1264 }
1265 }
1266 boolean forceInverse = !fullName && _join == JOIN_INVERSE;
1267 if (forceInverse) {
1268 local = foreign;
1269 foreign = table;
1270 inverse = true;
1271 }
1272
1273 // determine target
1274 String targetName = given.getTarget();
1275 Object target = null;
1276 Table ttable = null;
1277 boolean constant = false;
1278 boolean fullTarget = false;
1279 if (targetName == null && given.getTargetField() != null) {
1280 ClassMapping tcls = (inverse) ? cls : rel;
1281 String fieldName = given.getTargetField();
1282 int dotIdx = fieldName.lastIndexOf('.');
1283 fullTarget = dotIdx != -1;
1284
1285 if (dotIdx == 0) {
1286 // allow use of '.' without prefix to mean "use expected local
1287 // cls"; but if we already inversed no need to switch again
1288 if (!inverse)
1289 tcls = cls;
1290 fieldName = fieldName.substring(1);
1291 } else if (dotIdx > 0) {
1292 // must be class + field name
1293 tcls = findClassMapping(context, fieldName.substring
1294 (0, dotIdx), cls, rel);
1295 fieldName = fieldName.substring(dotIdx + 1);
1296 }
1297 if (tcls == null)
1298 throw new MetaDataException(_loc.get(prefix
1299 + "-bad-fktargetcls", context, fieldName, name));
1300
1301 FieldMapping field = tcls.getFieldMapping(fieldName);
1302 if (field == null)
1303 throw new MetaDataException(_loc.get(prefix
1304 + "-bad-fktargetfield", new Object[]{ context, fieldName,
1305 name, tcls }));
1306 if (field.getColumns().length != 1)
1307 throw new MetaDataException(_loc.get(prefix
1308 + "-fktargetfield-cols", context, fieldName, name));
1309 ttable = (field.getJoinForeignKey() != null) ? field.getTable()
1310 : field.getDefiningMapping().getTable();
1311 targetName = field.getColumns()[0].getName();
1312 } else if (targetName != null) {
1313 if (targetName.charAt(0) == '\'') {
1314 constant = true;
1315 target = targetName.substring(1, targetName.length() - 1);
1316 } else if (targetName.charAt(0) == '-'
1317 || targetName.charAt(0) == '.'
1318 || Character.isDigit(targetName.charAt(0))) {
1319 constant = true;
1320 try {
1321 if (targetName.indexOf('.') == -1)
1322 target = new Integer(targetName);
1323 else
1324 target = new Double(targetName);
1325 } catch (RuntimeException re) {
1326 throw new MetaDataException(_loc.get(prefix
1327 + "-bad-fkconst", context, targetName, name));
1328 }
1329 } else if ("null".equalsIgnoreCase(targetName))
1330 constant = true;
1331 else {
1332 int dotIdx = targetName.lastIndexOf('.');
1333 fullTarget = dotIdx != -1;
1334 if (dotIdx == 0) {
1335 // allow use of '.' without prefix to mean "use expected
1336 // local table", but ignore if we're already inversed
1337 if (!inverse)
1338 ttable = local;
1339 targetName = targetName.substring(1);
1340 } else if (dotIdx != -1) {
1341 ttable = findTable(context, targetName.substring(0,
1342 dotIdx), foreign, local, (inverse) ? cls : rel);
1343 targetName = targetName.substring(dotIdx + 1);
1344 }
1345 }
1346 }
1347
1348 // use explicit target table if available
1349 if (ttable == local && local != foreign) {
1350 // swap, unless user gave incompatible table in column name
1351 if (fullName)
1352 throw new MetaDataException(_loc.get(prefix
1353 + "-bad-fktarget-inverse", new Object[]{ context, name,
1354 foreign, ttable }));
1355 local = foreign;
1356 foreign = ttable;
1357 } else if (ttable != null) {
1358 // ttable might be a table of a base class of the target
1359 foreign = ttable;
1360 }
1361
1362 // check to see if we inversed; if this is a same-table join, then
1363 // consider it an implicit inverse if the user includes the table name
1364 // in the column name, but not in the column target, or if the user
1365 // gives no column name but a full target name
1366 inverse = inverse || local != table || (local == foreign
1367 && ((fullName && !fullTarget) || (name == null && fullTarget)));
1368 if (!inversable && !constant && inverse) {
1369 if (local == foreign)
1370 throw new MetaDataException(_loc.get(prefix
1371 + "-bad-fk-self-inverse", context, local));
1372 throw new MetaDataException(_loc.get(prefix + "-bad-fk-inverse",
1373 context, local, table));
1374 }
1375 if (name == null && constant)
1376 throw new MetaDataException(_loc.get(prefix
1377 + "-no-fkcol-name-adapt", context));
1378
1379 if (name == null && targetName == null) {
1380 // if no name or target is provided and there's more than one likely
1381 // join possibility, too ambiguous
1382 PrimaryKey pk = foreign.getPrimaryKey();
1383 if (joins.length != 1 || pk == null || pk.getColumns().length != 1)
1384 throw new MetaDataException(_loc.get(prefix
1385 + "-no-fkcol-name-adapt", context));
1386
1387 // assume target is pk column
1388 targetName = pk.getColumns()[0].getName();
1389 } else if (name != null && targetName == null) {
1390 // if one primary key column use it for target; if multiple joins
1391 // look for a foreign column with same name as local column
1392 PrimaryKey pk = foreign.getPrimaryKey();
1393 if (joins.length == 1 && pk != null && pk.getColumns().length == 1)
1394 targetName = pk.getColumns()[0].getName();
1395 else if (foreign.getColumn(name) != null)
1396 targetName = name;
1397 else
1398 throw new MetaDataException(_loc.get(prefix
1399 + "-no-fkcol-target-adapt", context, name));
1400 }
1401
1402 // find the target column, and create template for local column based
1403 // on it
1404 Column tmplate = new Column();
1405 tmplate.setName(name);
1406 if (!constant) {
1407 Column tcol = foreign.getColumn(targetName);
1408 if (tcol == null)
1409 throw new MetaDataException(_loc.get(prefix + "-bad-fktarget",
1410 new Object[]{ context, targetName, name, foreign }));
1411
1412 if (name == null)
1413 tmplate.setName(tcol.getName());
1414 tmplate.setJavaType(tcol.getJavaType());
1415 tmplate.setType(tcol.getType());
1416 tmplate.setTypeName(tcol.getTypeName());
1417 tmplate.setSize(tcol.getSize());
1418 tmplate.setDecimalDigits(tcol.getDecimalDigits());
1419 target = tcol;
1420 } else if (target instanceof String)
1421 tmplate.setJavaType(JavaTypes.STRING);
1422 else if (target instanceof Integer)
1423 tmplate.setJavaType(JavaTypes.INT);
1424 else if (target instanceof Double)
1425 tmplate.setJavaType(JavaTypes.DOUBLE);
1426
1427 // populate template, but let user-given name override default name
1428 if (def != null)
1429 def.populate(local, foreign, tmplate, target, inverse, idx,
1430 joins.length);
1431 if (name != null)
1432 tmplate.setName(name);
1433
1434 // create or merge local column
1435 Column col = mergeColumn(context, prefix, tmplate, true, given, local,
1436 adapt, fill);
1437
1438 joins[idx][0] = col;
1439 joins[idx][1] = target;
1440 if (inverse)
1441 joins[idx][2] = Boolean.TRUE;
1442 }
1443
1444 /**
1445 * Find the target class mapping given the user's class name.
1446 *
1447 * @param context for error messages
1448 * @param clsName class name given by user
1449 * @param cls original source mapping
1450 * @param rel original target mapping
1451 */
1452 private static ClassMapping findClassMapping(MetaDataContext context,
1453 String clsName, ClassMapping cls, ClassMapping rel) {
1454 if (isClassMappingName(clsName, cls))
1455 return cls;
1456 if (isClassMappingName(clsName, rel))
1457 return rel;
1458 throw new MetaDataException(_loc.get("target-wrong-cls", new Object[]
1459 { context, clsName, cls, rel }));
1460 }
1461
1462 /**
1463 * Return whether the given name matches the given mapping.
1464 */
1465 private static boolean isClassMappingName(String name, ClassMapping cls) {
1466 if (cls == null)
1467 return false;
1468 if (name.equals(cls.getDescribedType().getName())
1469 || name.equals(Strings.getClassName(cls.getDescribedType())))
1470 return true;
1471 return isClassMappingName(name, cls.getPCSuperclassMapping());
1472 }
1473
1474 /**
1475 * Sets internal column information to match the given mapped columns.
1476 *
1477 * @param forceJDBCType whether to force the jdbc-type of the columns
1478 * to be set, even when it matches the default for the columns' java type
1479 */
1480 protected void syncColumns(MetaDataContext context, Column[] cols,
1481 boolean forceJDBCType) {
1482 if (cols == null || cols.length == 0)
1483 _cols = null;
1484 else {
1485 _cols = new ArrayList(cols.length);
1486 Column col;
1487 for (int i = 0; i < cols.length; i++) {
1488 col = syncColumn(context, cols[i], cols.length,
1489 forceJDBCType, cols[i].getTable(), null, null, false);
1490 setColumnFlagsFromIO(col, i);
1491 _cols.add(col);
1492 }
1493 }
1494 }
1495
1496 /**
1497 * Set I/O flags on the column.
1498 */
1499 private void setColumnFlagsFromIO(Column col, int i) {
1500 if (_io == null)
1501 return;
1502 col.setFlag(Column.FLAG_UNUPDATABLE, !_io.isUpdatable(i, false));
1503 col.setFlag(Column.FLAG_UNINSERTABLE, !_io.isInsertable(i, false));
1504 }
1505
1506 /**
1507 * Sets internal index information to match given mapped index.
1508 */
1509 protected void syncIndex(MetaDataContext context, Index idx) {
1510 if (idx == null) {
1511 _idx = null;
1512 return;
1513 }
1514
1515 _canIdx = true;
1516 _idx = new Index();
1517 _idx.setName(idx.getName());
1518 _idx.setUnique(idx.isUnique());
1519 if (idx.getColumns() != null && idx.getColumns().length > 1)
1520 _idx.setColumns(idx.getColumns());
1521 }
1522
1523 /**
1524 * Sets internal constraint information to match given mapped constraint.
1525 */
1526 protected void syncUnique(MetaDataContext context, Unique unq) {
1527 if (unq == null) {
1528 _unq = null;
1529 return;
1530 }
1531
1532 _canUnq = true;
1533 _unq = new Unique();
1534 _unq.setName(unq.getName());
1535 _unq.setDeferred(unq.isDeferred());
1536 }
1537
1538 /**
1539 * Sets internal constraint and column information to match given mapped
1540 * constraint.
1541 *
1542 * @param local default local table
1543 * @param target default target table
1544 */
1545 protected void syncForeignKey(MetaDataContext context, ForeignKey fk,
1546 Table local, Table target) {
1547 if (fk == null) {
1548 _fk = null;
1549 _cols = null;
1550 _join = JOIN_NONE;
1551 return;
1552 }
1553 if (_join == JOIN_NONE)
1554 _join = JOIN_FORWARD;
1555
1556 if (fk.isLogical())
1557 _fk = null;
1558 else {
1559 _canFK = true;
1560 _fk = new ForeignKey();
1561 _fk.setName(fk.getName());
1562 _fk.setDeleteAction(fk.getDeleteAction());
1563 _fk.setUpdateAction(fk.getUpdateAction());
1564 _fk.setDeferred(fk.isDeferred());
1565 }
1566
1567 Column[] cols = fk.getColumns();
1568 Column[] pkCols = fk.getPrimaryKeyColumns();
1569 Column[] ccols = fk.getConstantColumns();
1570 Object[] cs = fk.getConstants();
1571 Column[] cpkCols = fk.getConstantPrimaryKeyColumns();
1572 Object[] cpks = fk.getPrimaryKeyConstants();
1573
1574 int size = cols.length + ccols.length + cpkCols.length;
1575 _cols = new ArrayList(size);
1576 Column col;
1577 for (int i = 0; i < cols.length; i++) {
1578 col = syncColumn(context, cols[i], size, false, local,
1579 target, pkCols[i], _join == JOIN_INVERSE);
1580 setColumnFlagsFromIO(col, i);
1581 _cols.add(col);
1582 }
1583 Object constant;
1584 for (int i = 0; i < ccols.length; i++) {
1585 constant = (cs[i] == null) ? NULL : cs[i];
1586 col = syncColumn(context, ccols[i], size, false, local,
1587 target, constant, _join == JOIN_INVERSE);
1588 setColumnFlagsFromIO(col, cols.length + i);
1589 _cols.add(col);
1590 }
1591 for (int i = 0; i < cpkCols.length; i++) {
1592 constant = (cpks[i] == null) ? NULL : cpks[i];
1593 _cols.add(syncColumn(context, cpkCols[i], size, false, target,
1594 local, constant, _join != JOIN_INVERSE));
1595 }
1596 }
1597
1598 /**
1599 * Create a copy of the given column with the raw mapping information
1600 * set correctly, and without settings that match defaults.
1601 *
1602 * @param num the number of columns for this mapping
1603 * @param forceJDBCType whether the jdbc-type of the created column
1604 * should be set, even if it matches the default
1605 * for the given column's java type
1606 * @param colTable expected table for the column
1607 * @param targetTable expected target table for join column
1608 * @param target target column or object for join column; for a
1609 * constant null target, use {@link #NULL}
1610 * @param inverse whether join column is for inverse join
1611 */
1612 protected static Column syncColumn(MetaDataContext context, Column col,
1613 int num, boolean forceJDBCType, Table colTable, Table targetTable,
1614 Object target, boolean inverse) {
1615 // use full name for cols that aren't in the expected table, or that
1616 // are inverse joins
1617 DBDictionary dict = ((MappingRepository) context.getRepository()).
1618 getDBDictionary();
1619 Column copy = new Column();
1620 if (col.getTable() != colTable || inverse)
1621 copy.setName(dict.getFullName(col.getTable(), true)
1622 + "." + col.getName());
1623 else
1624 copy.setName(col.getName());
1625
1626 // set target if not default
1627 if (target != null) {
1628 if (target == NULL)
1629 copy.setTarget("null");
1630 else if (target instanceof Column) {
1631 Column tcol = (Column) target;
1632 if ((!inverse && tcol.getTable() != targetTable)
1633 || (inverse && tcol.getTable() != colTable))
1634 copy.setTarget(dict.getFullName(tcol.getTable(), true)
1635 + "." + tcol.getName());
1636 else if (!defaultTarget(col, tcol, num))
1637 copy.setTarget(tcol.getName());
1638 } else if (target instanceof Number)
1639 copy.setTarget(target.toString());
1640 else
1641 copy.setTarget("'" + target + "'");
1642 } else if (num > 1)
1643 copy.setTargetField(col.getTargetField());
1644
1645 if (col.getSize() != 0 && col.getSize() != dict.characterColumnSize
1646 && (col.getSize() != -1 || !col.isLob()))
1647 copy.setSize(col.getSize());
1648 if (col.getDecimalDigits() != 0)
1649 copy.setDecimalDigits(col.getDecimalDigits());
1650 if (col.getDefaultString() != null)
1651 copy.setDefaultString(col.getDefaultString());
1652 if (col.isNotNull() && !col.isPrimaryKey()
1653 && (!isPrimitive(col.getJavaType()) || isForeignKey(col)))
1654 copy.setNotNull(true);
1655
1656 // set type name if not default
1657 String typeName = col.getTypeName();
1658 if (typeName != null || copy.getSize() != 0
1659 || copy.getDecimalDigits() != 0) {
1660 // is this the dict default type? have to ensure jdbc-type set
1661 // prior to finding dict default
1662 copy.setType(col.getType());
1663 String defName = dict.getTypeName(copy);
1664 copy.setType(Types.OTHER);
1665
1666 // copy should not have size info set if it isn't used in type name
1667 boolean defSized = defName.indexOf('(') != -1;
1668 if (!defSized) {
1669 if (copy.getSize() > 0)
1670 copy.setSize(0);
1671 copy.setDecimalDigits(0);
1672 }
1673
1674 if (typeName != null) {
1675 // make sure to strip size for comparison
1676 if (typeName.indexOf('(') == -1 && defSized)
1677 defName = defName.substring(0, defName.indexOf('('));
1678 if (!typeName.equalsIgnoreCase(defName))
1679 copy.setTypeName(typeName);
1680 }
1681 }
1682
1683 // set jdbc-type if not default or if forced
1684 if (forceJDBCType
1685 || (target != null && !(target instanceof Column)
1686 && col.getType() != Types.VARCHAR)
1687 || dict.getJDBCType(col.getJavaType(), false) != col.getType())
1688 copy.setType(col.getType());
1689
1690 return copy;
1691 }
1692
1693 /**
1694 * Return whether the given column belongs to a foreign key.
1695 */
1696 private static boolean isForeignKey(Column col)
1697 {
1698 if (col.getTable() == null)
1699 return false;
1700 ForeignKey[] fks = col.getTable().getForeignKeys();
1701 for (int i = 0; i < fks.length; i++)
1702 if (fks[i].containsColumn(col)
1703 || fks[i].containsConstantColumn(col))
1704 return true;
1705 return false;
1706 }
1707
1708 /**
1709 * Return true if the given type code represents a primitive.
1710 */
1711 private static boolean isPrimitive(int type) {
1712 switch (type) {
1713 case JavaTypes.BOOLEAN:
1714 case JavaTypes.BYTE:
1715 case JavaTypes.CHAR:
1716 case JavaTypes.DOUBLE:
1717 case JavaTypes.FLOAT:
1718 case JavaTypes.INT:
1719 case JavaTypes.LONG:
1720 case JavaTypes.SHORT:
1721 return true;
1722 default:
1723 return false;
1724 }
1725 }
1726
1727 /**
1728 * Return true if the given target column matches the default.
1729 * If there is only one column involved in the join and it links to the
1730 * single target table pk column, or if the column name is the same as
1731 * the target column name, then the target is the default.
1732 */
1733 private static boolean defaultTarget(Column col, Column targetCol,
1734 int num) {
1735 if (col.getName().equals(targetCol.getName()))
1736 return true;
1737 if (num > 1)
1738 return false;
1739
1740 PrimaryKey pk = targetCol.getTable().getPrimaryKey();
1741 if (pk == null || pk.getColumns().length != 1)
1742 return false;
1743 return targetCol == pk.getColumns()[0];
1744 }
1745
1746 /**
1747 * Supplies default table information.
1748 */
1749 public static interface TableDefaults {
1750
1751 /**
1752 * Return the default table name.
1753 */
1754 public String get(Schema schema);
1755 }
1756
1757 /**
1758 * Supplies default foreign key information.
1759 */
1760 public static interface ForeignKeyDefaults {
1761
1762 /**
1763 * Return a default foreign key for the given tables, or null to
1764 * create a logical foreign key only. Do not fill in the columns of
1765 * the foreign key, only attributes like its name, delete action, etc.
1766 * Do not add the foreign key to the table.
1767 */
1768 public ForeignKey get(Table local, Table foreign, boolean inverse);
1769
1770 /**
1771 * Populate the given foreign key column with defaults.
1772 *
1773 * @param target the target column or constant value
1774 * @param pos the index of this column in the foreign key
1775 * @param cols the number of columns in the foreign key
1776 */
1777 public void populate(Table local, Table foreign, Column col,
1778 Object target, boolean inverse, int pos, int cols);
1779 }
1780 }