1 //$Id: Ejb3JoinColumn.java 11223 2007-02-20 19:37:47Z epbernard $
2 package org.hibernate.cfg;
3
4 import java.util.HashSet;
5 import java.util.Iterator;
6 import java.util.Map;
7 import java.util.Set;
8 import javax.persistence.JoinColumn;
9 import javax.persistence.PrimaryKeyJoinColumn;
10
11 import org.hibernate.AnnotationException;
12 import org.hibernate.MappingException;
13 import org.hibernate.annotations.common.util.StringHelper;
14 import org.hibernate.mapping.Column;
15 import org.hibernate.mapping.Join;
16 import org.hibernate.mapping.PersistentClass;
17 import org.hibernate.mapping.SimpleValue;
18 import org.hibernate.mapping.Table;
19 import org.hibernate.mapping.Value;
20
21 /**
22 * Wrap state of an EJB3 @JoinColumn annotation
23 * and build the Hibernate column mapping element
24 *
25 * @author Emmanuel Bernard
26 */
27 public class Ejb3JoinColumn extends Ejb3Column {
28 /**
29 * property name repated to this column
30 */
31 private String referencedColumn;
32 private String mappedBy;
33 //property name on the mapped by side if any
34 private String mappedByPropertyName;
35 //table name on the mapped by side if any
36 private String mappedByTableName;
37 private String mappedByEntityName;
38
39 //FIXME hacky solution to get the information at proeprty ref resolution
40 public String getManyToManyOwnerSideEntityName() {
41 return manyToManyOwnerSideEntityName;
42 }
43
44 public void setManyToManyOwnerSideEntityName(String manyToManyOwnerSideEntityName) {
45 this.manyToManyOwnerSideEntityName = manyToManyOwnerSideEntityName;
46 }
47
48 private String manyToManyOwnerSideEntityName;
49
50 public void setReferencedColumn(String referencedColumn) {
51 this.referencedColumn = referencedColumn;
52 }
53
54 public String getMappedBy() {
55 return mappedBy;
56 }
57
58 public void setMappedBy(String mappedBy) {
59 this.mappedBy = mappedBy;
60 }
61
62 //Due to @AnnotationOverride overriding rules, I don't want the constructor to be public
63 private Ejb3JoinColumn() {
64 setMappedBy( BinderHelper.ANNOTATION_STRING_DEFAULT );
65 }
66
67 //Due to @AnnotationOverride overriding rules, I don't want the constructor to be public
68 //TODO get rid of it and use setters
69 private Ejb3JoinColumn(
70 String sqlType,
71 String name,
72 boolean nullable,
73 boolean unique,
74 boolean insertable,
75 boolean updatable,
76 String referencedColumn,
77 String secondaryTable,
78 Map<String, Join> joins,
79 PropertyHolder propertyHolder,
80 String propertyName,
81 String mappedBy,
82 boolean isImplicit,
83 ExtendedMappings mappings
84 ) {
85 super();
86 setImplicit( isImplicit );
87 setSqlType( sqlType );
88 setLogicalColumnName( name );
89 setNullable( nullable );
90 setUnique( unique );
91 setInsertable( insertable );
92 setUpdatable( updatable );
93 setSecondaryTableName( secondaryTable );
94 setPropertyHolder( propertyHolder );
95 setJoins( joins );
96 setMappings( mappings );
97 setPropertyName( BinderHelper.getRelativePath( propertyHolder, propertyName ) );
98 bind();
99 this.referencedColumn = referencedColumn;
100 this.mappedBy = mappedBy;
101 }
102
103 public String getReferencedColumn() {
104 return referencedColumn;
105 }
106
107 public static Ejb3JoinColumn[] buildJoinColumns(
108 JoinColumn[] anns,
109 String mappedBy, Map<String, Join> joins,
110 PropertyHolder propertyHolder,
111 String propertyName,
112 ExtendedMappings mappings
113 ) {
114 JoinColumn[] actualColumns = propertyHolder.getOverriddenJoinColumn(
115 StringHelper.qualify( propertyHolder.getPath(), propertyName )
116 );
117 if ( actualColumns == null ) actualColumns = anns;
118 if ( actualColumns == null || actualColumns.length == 0 ) {
119 return new Ejb3JoinColumn[]{
120 buildJoinColumn( (JoinColumn) null, mappedBy, joins, propertyHolder, propertyName, mappings )
121 };
122 }
123 else {
124 int size = actualColumns.length;
125 Ejb3JoinColumn[] result = new Ejb3JoinColumn[ size ];
126 for ( int index = 0; index < size ; index++ ) {
127 result[index] = buildJoinColumn(
128 actualColumns[index],
129 mappedBy,
130 joins,
131 propertyHolder,
132 propertyName,
133 mappings
134 );
135 }
136 return result;
137 }
138 }
139
140 /**
141 * build join column for SecondaryTables
142 */
143 private static Ejb3JoinColumn buildJoinColumn(
144 JoinColumn ann,
145 String mappedBy, Map<String, Join> joins,
146 PropertyHolder propertyHolder,
147 String propertyName,
148 ExtendedMappings mappings
149 ) {
150 if ( ann != null ) {
151 if ( BinderHelper.isDefault( mappedBy ) ) {
152 throw new AnnotationException(
153 "Illegal attempt to define a @JoinColumn with a mappedBy association: "
154 + BinderHelper.getRelativePath( propertyHolder, propertyName )
155 );
156 }
157 Ejb3JoinColumn joinColumn = new Ejb3JoinColumn();
158 joinColumn.setJoinAnnotation( ann, null );
159 joinColumn.setJoins( joins );
160 joinColumn.setPropertyHolder( propertyHolder );
161 joinColumn.setPropertyName( BinderHelper.getRelativePath( propertyHolder, propertyName ) );
162 joinColumn.setImplicit( false );
163 joinColumn.setMappings( mappings );
164 joinColumn.bind();
165 return joinColumn;
166 }
167 else {
168 Ejb3JoinColumn joinColumn = new Ejb3JoinColumn();
169 joinColumn.setMappedBy( mappedBy );
170 joinColumn.setJoins( joins );
171 joinColumn.setPropertyHolder( propertyHolder );
172 joinColumn.setPropertyName( BinderHelper.getRelativePath( propertyHolder, propertyName ) );
173 joinColumn.setImplicit( true );
174 joinColumn.setMappings( mappings );
175 joinColumn.bind();
176 return joinColumn;
177 }
178 }
179
180
181 //FIXME default name still useful in association table
182 public void setJoinAnnotation(JoinColumn annJoin, String defaultName) {
183 if ( annJoin == null ) {
184 setImplicit( true );
185 }
186 else {
187 setImplicit( false );
188 if ( ! BinderHelper.isDefault( annJoin.columnDefinition() ) ) setSqlType( annJoin.columnDefinition() );
189 if ( ! BinderHelper.isDefault( annJoin.name() ) ) setLogicalColumnName( annJoin.name() );
190 setNullable( annJoin.nullable() );
191 setUnique( annJoin.unique() );
192 setInsertable( annJoin.insertable() );
193 setUpdatable( annJoin.updatable() );
194 setReferencedColumn( annJoin.referencedColumnName() );
195 setSecondaryTableName( annJoin.table() );
196 }
197 }
198
199 /**
200 * Build JoinColumn for a JOINED hierarchy
201 */
202 public static Ejb3JoinColumn buildJoinColumn(
203 PrimaryKeyJoinColumn pkJoinAnn,
204 JoinColumn joinAnn,
205 Value identifier,
206 Map<String, Join> joins,
207 PropertyHolder propertyHolder, ExtendedMappings mappings
208 ) {
209
210 Column col = (Column) identifier.getColumnIterator().next();
211 String defaultName = mappings.getLogicalColumnName( col.getName(), identifier.getTable() );
212 if ( pkJoinAnn != null || joinAnn != null ) {
213 String colName;
214 String columnDefinition;
215 String referencedColumnName;
216 if ( pkJoinAnn != null ) {
217 colName = pkJoinAnn.name();
218 columnDefinition = pkJoinAnn.columnDefinition();
219 referencedColumnName = pkJoinAnn.referencedColumnName();
220 }
221 else {
222 colName = joinAnn.name();
223 columnDefinition = joinAnn.columnDefinition();
224 referencedColumnName = joinAnn.referencedColumnName();
225 }
226 String sqlType = "".equals( columnDefinition ) ? null : columnDefinition;
227 String name = "".equals( colName ) ? defaultName : colName;
228 return new Ejb3JoinColumn(
229 sqlType,
230 name, false, false,
231 true, true,
232 referencedColumnName,
233 null, joins,
234 propertyHolder, null, null, false, mappings
235 );
236 }
237 else {
238 return new Ejb3JoinColumn(
239 (String) null, defaultName,
240 false, false, true, true, null, (String) null,
241 joins, propertyHolder, null, null, true, mappings
242 );
243 }
244 }
245
246 /**
247 * Override persistent class on oneToMany Cases for late settings
248 * Must only be used on second level pass binding
249 */
250 public void setPersistentClass(PersistentClass persistentClass, Map<String, Join> joins) {
251 //FIXME shouldn't we deduce the classname from the persistentclasS?
252 this.propertyHolder = PropertyHolderBuilder.buildPropertyHolder( persistentClass, joins, getMappings() );
253 }
254
255 public static void checkIfJoinColumn(Object columns, PropertyHolder holder, PropertyData property) {
256 if ( ! ( columns instanceof Ejb3JoinColumn[] ) ) {
257 throw new AnnotationException(
258 "@Column cannot be used on an association property: "
259 + holder.getEntityName()
260 + "."
261 + property.getPropertyName()
262 );
263 }
264 }
265
266 public void linkValueUsingDefaultColumnNaming(
267 Column referencedColumn, PersistentClass referencedEntity, SimpleValue value
268 ) {
269 String columnName;
270 String logicalReferencedColumn = getMappings().getLogicalColumnName(
271 referencedColumn.getName(), referencedEntity.getTable()
272 );
273 boolean mappedBySide = mappedByTableName != null || mappedByPropertyName != null;
274 boolean ownerSide = getPropertyName() != null;
275
276 Boolean isRefColumnQuoted = StringHelper.isQuoted( logicalReferencedColumn );
277 String unquotedLogicalReferenceColumn = isRefColumnQuoted ?
278 StringHelper.unquote( logicalReferencedColumn ) :
279 logicalReferencedColumn;
280
281 if ( mappedBySide ) {
282 String unquotedMappedbyTable = StringHelper.unquote( mappedByTableName );
283 columnName = getMappings().getNamingStrategy().foreignKeyColumnName(
284 mappedByPropertyName,
285 mappedByEntityName,
286 unquotedMappedbyTable,
287 unquotedLogicalReferenceColumn
288 );
289 //one element was quoted so we quote
290 if ( isRefColumnQuoted || StringHelper.isQuoted( mappedByTableName ) ) {
291 columnName = StringHelper.quote( columnName );
292 }
293 }
294 else if ( ownerSide ) {
295 String logicalTableName = getMappings().getLogicalTableName( referencedEntity.getTable() );
296 String unquotedLogicalTableName = StringHelper.unquote( logicalTableName );
297 columnName = getMappings().getNamingStrategy().foreignKeyColumnName(
298 getPropertyName(),
299 referencedEntity.getEntityName(),
300 unquotedLogicalTableName,
301 unquotedLogicalReferenceColumn
302 );
303 //one element was quoted so we quote
304 if ( isRefColumnQuoted || StringHelper.isQuoted( logicalTableName ) ) {
305 columnName = StringHelper.quote( columnName );
306 }
307 }
308 else {
309 //is an intra-entity hierarchy table join so copy the name by default
310 String logicalTableName = getMappings().getLogicalTableName( referencedEntity.getTable() );
311 String unquotedLogicalTableName = StringHelper.unquote( logicalTableName );
312 columnName = getMappings().getNamingStrategy().joinKeyColumnName(
313 unquotedLogicalReferenceColumn,
314 unquotedLogicalTableName
315 );
316 //one element was quoted so we quote
317 if ( isRefColumnQuoted || StringHelper.isQuoted( logicalTableName ) ) {
318 columnName = StringHelper.quote( columnName );
319 }
320 }
321 //yuk side effect on an implicit column
322 setLogicalColumnName( columnName );
323 setReferencedColumn( logicalReferencedColumn );
324 initMappingColumn(
325 columnName,
326 null, referencedColumn.getLength(),
327 referencedColumn.getPrecision(),
328 referencedColumn.getScale(),
329 getMappingColumn().isNullable(),
330 referencedColumn.getSqlType(),
331 getMappingColumn().isUnique(), false
332 );
333 linkWithValue( value );
334 }
335
336 /**
337 * used for mappedBy cases
338 */
339 public void linkValueUsingAColumnCopy(Column column, SimpleValue value) {
340 initMappingColumn(
341 //column.getName(),
342 column.getQuotedName(),
343 null, column.getLength(),
344 column.getPrecision(),
345 column.getScale(),
346 getMappingColumn().isNullable(),
347 column.getSqlType(),
348 getMappingColumn().isUnique(),
349 false //We do copy no strategy here
350 );
351 linkWithValue( value );
352 }
353
354 protected void addColumnBinding(SimpleValue value) {
355 if ( StringHelper.isEmpty( mappedBy ) ) {
356 String unquotedLogColName = StringHelper.unquote( getLogicalColumnName() );
357 String unquotedRefColumn = StringHelper.unquote( getReferencedColumn() );
358 String logicalColumnName = getMappings().getNamingStrategy()
359 .logicalCollectionColumnName( unquotedLogColName, getPropertyName(), unquotedRefColumn );
360 if ( StringHelper.isQuoted( getLogicalColumnName() ) || StringHelper.isQuoted( getLogicalColumnName() ) ) {
361 logicalColumnName = StringHelper.quote( logicalColumnName );
362 }
363 getMappings().addColumnBinding( logicalColumnName, getMappingColumn(), value.getTable() );
364 }
365 }
366
367 //keep it JDK 1.4 compliant
368 //implicit way
369 public static final int NO_REFERENCE = 0;
370 //reference to the pk in an explicit order
371 public static final int PK_REFERENCE = 1;
372 //reference to non pk columns
373 public static final int NON_PK_REFERENCE = 2;
374
375 public static int checkReferencedColumnsType(
376 Ejb3JoinColumn[] columns, PersistentClass referencedEntity,
377 ExtendedMappings mappings
378 ) {
379 //convenient container to find whether a column is an id one or not
380 Set<Column> idColumns = new HashSet<Column>();
381 Iterator idColumnsIt = referencedEntity.getKey().getColumnIterator();
382 while ( idColumnsIt.hasNext() ) {
383 idColumns.add( (Column) idColumnsIt.next() );
384 }
385
386 boolean isFkReferencedColumnName = false;
387 boolean noReferencedColumn = true;
388 //build the list of potential tables
389 if ( columns.length == 0 ) return NO_REFERENCE; //shortcut
390 Object columnOwner = BinderHelper.findColumnOwner(
391 referencedEntity, columns[0].getReferencedColumn(), mappings
392 );
393 if ( columnOwner == null ) {
394 throw new MappingException(
395 "Unable to find column with logical name: "
396 + columns[0].getReferencedColumn() + " in " + referencedEntity.getTable() + " and its related "
397 + "supertables and secondary tables"
398 );
399 }
400 Table matchingTable = columnOwner instanceof PersistentClass ?
401 ( (PersistentClass) columnOwner ).getTable() :
402 ( (Join) columnOwner ).getTable();
403 //check each referenced column
404 for ( Ejb3JoinColumn ejb3Column : columns ) {
405 String logicalReferencedColumnName = ejb3Column.getReferencedColumn();
406 if ( StringHelper.isNotEmpty( logicalReferencedColumnName ) ) {
407 String referencedColumnName = null;
408 try {
409 referencedColumnName = mappings.getPhysicalColumnName( logicalReferencedColumnName, matchingTable );
410 }
411 catch (MappingException me) {
412 //rewrite the exception
413 throw new MappingException(
414 "Unable to find column with logical name: "
415 + logicalReferencedColumnName + " in " + matchingTable.getName()
416 );
417 }
418 noReferencedColumn = false;
419 Column refCol = new Column( referencedColumnName );
420 boolean contains = idColumns.contains( refCol );
421 if ( ! contains ) {
422 isFkReferencedColumnName = true;
423 break; //we know the state
424 }
425 }
426 }
427 if ( isFkReferencedColumnName ) {
428 return NON_PK_REFERENCE;
429 }
430 else if ( noReferencedColumn ) {
431 return NO_REFERENCE;
432 }
433 else if ( idColumns.size() != columns.length ) {
434 //reference use PK but is a subset or a superset
435 return NON_PK_REFERENCE;
436 } else {
437 return PK_REFERENCE;
438 }
439 }
440
441 public void overrideSqlTypeIfNecessary(org.hibernate.mapping.Column column) {
442 if ( StringHelper.isEmpty( sqlType ) ) {
443 sqlType = column.getSqlType();
444 if ( getMappingColumn() != null ) getMappingColumn().setSqlType( sqlType );
445 }
446 }
447
448 @Override
449 public void redefineColumnName(String columnName, String propertyName, boolean applyNamingStrategy) {
450 if ( StringHelper.isNotEmpty( columnName ) ) {
451 getMappingColumn().setName(
452 applyNamingStrategy ?
453 getMappings().getNamingStrategy().columnName( columnName ) :
454 columnName
455 );
456 }
457 }
458
459 public static Ejb3JoinColumn[] buildJoinTableJoinColumns(
460 JoinColumn[] annJoins, Map<String, Join> secondaryTables,
461 PropertyHolder propertyHolder, String propertyName, String mappedBy, ExtendedMappings mappings
462 ) {
463 Ejb3JoinColumn[] joinColumns;
464 if ( annJoins == null ) {
465 Ejb3JoinColumn currentJoinColumn = new Ejb3JoinColumn();
466 currentJoinColumn.setImplicit( true );
467 currentJoinColumn.setNullable( false ); //I break the spec, but it's for good
468 currentJoinColumn.setPropertyHolder( propertyHolder );
469 currentJoinColumn.setJoins( secondaryTables );
470 currentJoinColumn.setMappings( mappings );
471 currentJoinColumn.setPropertyName(
472 BinderHelper.getRelativePath( propertyHolder, propertyName )
473 );
474 currentJoinColumn.setMappedBy( mappedBy );
475 currentJoinColumn.bind();
476
477 joinColumns = new Ejb3JoinColumn[]{
478 currentJoinColumn
479
480 };
481 }
482 else {
483 joinColumns = new Ejb3JoinColumn[annJoins.length];
484 JoinColumn annJoin;
485 int length = annJoins.length;
486 for ( int index = 0; index < length ; index++ ) {
487 annJoin = annJoins[index];
488 Ejb3JoinColumn currentJoinColumn = new Ejb3JoinColumn();
489 currentJoinColumn.setImplicit( true );
490 currentJoinColumn.setPropertyHolder( propertyHolder );
491 currentJoinColumn.setJoins( secondaryTables );
492 currentJoinColumn.setMappings( mappings );
493 currentJoinColumn.setPropertyName( BinderHelper.getRelativePath( propertyHolder, propertyName ) );
494 currentJoinColumn.setMappedBy( mappedBy );
495 currentJoinColumn.setJoinAnnotation( annJoin, propertyName );
496 currentJoinColumn.setNullable( false ); //I break the spec, but it's for good
497 //done after the annotation to override it
498 currentJoinColumn.bind();
499 joinColumns[index] = currentJoinColumn;
500 }
501 }
502 return joinColumns;
503 }
504
505 public void setMappedBy(String entityName, String logicalTableName, String mappedByProperty) {
506 this.mappedByEntityName = entityName;
507 this.mappedByTableName = logicalTableName;
508 this.mappedByPropertyName = mappedByProperty;
509 }
510 }