001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.ejb.plugins.cmp.jdbc.metadata;
023:
024: import java.util.ArrayList;
025: import java.util.Iterator;
026: import javax.ejb.EJBException;
027: import javax.naming.InitialContext;
028: import javax.naming.NamingException;
029: import javax.sql.DataSource;
030: import org.jboss.deployment.DeploymentException;
031: import org.jboss.metadata.MetaData;
032: import org.jboss.metadata.RelationMetaData;
033: import org.jboss.metadata.RelationshipRoleMetaData;
034: import org.w3c.dom.Element;
035:
036: /**
037: * This class represents one ejb-relation element in the ejb-jar.xml file. Most
038: * properties of this class are immutable. The mutable properties have set
039: * methods.
040: *
041: * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom </a>
042: * @author <a href="mailto:heiko.rupp@cellent.de">Heiko W. Rupp </a>
043: * @version $Revision: 57209 $
044: */
045: public final class JDBCRelationMetaData {
046: private final static int TABLE = 1;
047:
048: private final static int FOREIGN_KEY = 2;
049:
050: /** Name of the relation. Loaded from the ejb-relation-name element. */
051: private final String relationName;
052:
053: /**
054: * The left jdbc relationship role. Loaded from an ejb-relationship-role.
055: * Left/right assignment is completely arbitrary.
056: */
057: private final JDBCRelationshipRoleMetaData left;
058:
059: /**
060: * The right relationship role. Loaded from an ejb-relationship-role.
061: * Left/right assignment is completely arbitrary.
062: */
063: private final JDBCRelationshipRoleMetaData right;
064:
065: /**
066: * The mapping style for this relation (i.e., TABLE or FOREIGN_KEY).
067: */
068: private final int mappingStyle;
069:
070: /** data source name in jndi */
071: private final String dataSourceName;
072:
073: /** datasource type mapping name is defined in the deployment descriptor */
074: private final String datasourceMappingName;
075:
076: /** This is a cache of the datasource object. */
077: private transient DataSource dataSource;
078:
079: /** type mapping used for the relation table */
080: private final JDBCTypeMappingMetaData datasourceMapping;
081:
082: /** the name of the table to use for this bean */
083: private final String tableName;
084:
085: /** is table created */
086: private boolean tableCreated;
087:
088: /** is table dropped */
089: private boolean tableDropped;
090:
091: /** should we create the table when deployed */
092: private final boolean createTable;
093:
094: /** should we drop the table when deployed */
095: private final boolean removeTable;
096:
097: /** should we alter the table when deployed */
098: private final boolean alterTable;
099:
100: /**
101: * What commands should be issued directly after creation of a table?
102: */
103: private final ArrayList tablePostCreateCmd;
104:
105: /** should we use 'SELECT ... FOR UPDATE' syntax? */
106: private final boolean rowLocking;
107:
108: /** should the table have a primary key constraint? */
109: private final boolean primaryKeyConstraint;
110:
111: /** is the relationship read-only? */
112: private final boolean readOnly;
113:
114: /** how long is read valid */
115: private final int readTimeOut;
116:
117: /**
118: * Constructs jdbc relation meta data with the data from the relation
119: * metadata loaded from the ejb-jar.xml file.
120: *
121: * @param jdbcApplication used to retrieve the entities of this relation
122: * @param relationMetaData relation meta data loaded from the ejb-jar.xml
123: * file
124: */
125: public JDBCRelationMetaData(
126: JDBCApplicationMetaData jdbcApplication,
127: RelationMetaData relationMetaData)
128: throws DeploymentException {
129:
130: relationName = relationMetaData.getRelationName();
131:
132: RelationshipRoleMetaData leftRole = relationMetaData
133: .getLeftRelationshipRole();
134: RelationshipRoleMetaData rightRole = relationMetaData
135: .getRightRelationshipRole();
136:
137: // set the default mapping style
138: if (leftRole.isMultiplicityMany()
139: && rightRole.isMultiplicityMany()) {
140: mappingStyle = TABLE;
141: } else {
142: mappingStyle = FOREIGN_KEY;
143: }
144:
145: dataSourceName = null;
146: datasourceMappingName = null;
147: datasourceMapping = null;
148: createTable = false;
149: removeTable = false;
150: alterTable = false;
151: rowLocking = false;
152: primaryKeyConstraint = false;
153: readOnly = false;
154: readTimeOut = -1;
155:
156: left = new JDBCRelationshipRoleMetaData(this , jdbcApplication,
157: leftRole);
158:
159: right = new JDBCRelationshipRoleMetaData(this , jdbcApplication,
160: rightRole);
161: left.init(right);
162: right.init(left);
163:
164: if (mappingStyle == TABLE) {
165: tableName = createDefaultTableName();
166: tablePostCreateCmd = getDefaultTablePostCreateCmd();
167: } else {
168: tableName = null;
169: tablePostCreateCmd = null;
170: }
171: }
172:
173: /**
174: * Constructs relation meta data with the data contained in the ejb-relation
175: * element or the defaults element from a jbosscmp-jdbc xml file. Optional
176: * values of the xml element that are not present are loaded from the
177: * defaultValues parameter.
178: *
179: * @param jdbcApplication used to retrieve type mappings in table mapping
180: * style
181: * @param element the xml Element which contains the metadata about this
182: * relation
183: * @param defaultValues the JDBCApplicationMetaData which contains the
184: * values for optional elements of the element
185: * @throws DeploymentException if the xml element is not semantically
186: * correct
187: */
188: public JDBCRelationMetaData(
189: JDBCApplicationMetaData jdbcApplication, Element element,
190: JDBCRelationMetaData defaultValues)
191: throws DeploymentException {
192:
193: relationName = defaultValues.getRelationName();
194: mappingStyle = loadMappingStyle(element, defaultValues);
195:
196: // read-only
197: String readOnlyString = MetaData.getOptionalChildContent(
198: element, "read-only");
199: if (readOnlyString != null) {
200: readOnly = Boolean.valueOf(readOnlyString).booleanValue();
201: } else {
202: readOnly = defaultValues.isReadOnly();
203: }
204:
205: // read-time-out
206: String readTimeOutString = MetaData.getOptionalChildContent(
207: element, "read-time-out");
208: if (readTimeOutString != null) {
209: try {
210: readTimeOut = Integer.parseInt(readTimeOutString);
211: } catch (NumberFormatException e) {
212: throw new DeploymentException(
213: "Invalid number format in " + "read-time-out '"
214: + readTimeOutString + "': " + e);
215: }
216: } else {
217: readTimeOut = defaultValues.getReadTimeOut();
218: }
219:
220: //
221: // Load all of the table options. defaults and relation-table-mapping
222: // will have these elements, and foreign-key will get the default values.
223: //
224: Element mappingElement = getMappingElement(element);
225:
226: // datasource name
227: String dataSourceNameString = MetaData.getOptionalChildContent(
228: mappingElement, "datasource");
229: if (dataSourceNameString != null)
230: dataSourceName = dataSourceNameString;
231: else
232: dataSourceName = defaultValues.getDataSourceName();
233:
234: // get the type mapping for this datasource (optional, but always
235: // set in standardjbosscmp-jdbc.xml)
236: String datasourceMappingString = MetaData
237: .getOptionalChildContent(mappingElement,
238: "datasource-mapping");
239: if (datasourceMappingString != null) {
240: datasourceMappingName = datasourceMappingString;
241: datasourceMapping = jdbcApplication
242: .getTypeMappingByName(datasourceMappingString);
243: if (datasourceMapping == null) {
244: throw new DeploymentException(
245: "Error in jbosscmp-jdbc.xml : "
246: + "datasource-mapping "
247: + datasourceMappingString
248: + " not found");
249: }
250: } else if (defaultValues.datasourceMappingName != null
251: && defaultValues.getTypeMapping() != null) {
252: datasourceMappingName = null;
253: datasourceMapping = defaultValues.getTypeMapping();
254: } else {
255: datasourceMappingName = null;
256: datasourceMapping = JDBCEntityMetaData
257: .obtainTypeMappingFromLibrary(dataSourceName);
258: }
259:
260: // get table name
261: String tableNameString = MetaData.getOptionalChildContent(
262: mappingElement, "table-name");
263: if (tableNameString == null) {
264: tableNameString = defaultValues.getDefaultTableName();
265: if (tableNameString == null) {
266: // use defaultValues to create default, because left/right
267: // have not been assigned yet, and values used to generate
268: // default table name never change
269: tableNameString = defaultValues
270: .createDefaultTableName();
271: }
272: }
273: tableName = tableNameString;
274:
275: // create table? If not provided, keep default.
276: String createString = MetaData.getOptionalChildContent(
277: mappingElement, "create-table");
278: if (createString != null) {
279: createTable = Boolean.valueOf(createString).booleanValue();
280: } else {
281: createTable = defaultValues.getCreateTable();
282: }
283:
284: // remove table? If not provided, keep default.
285: String removeString = MetaData.getOptionalChildContent(
286: mappingElement, "remove-table");
287: if (removeString != null) {
288: removeTable = Boolean.valueOf(removeString).booleanValue();
289: } else {
290: removeTable = defaultValues.getRemoveTable();
291: }
292:
293: // post-table-create commands
294: Element posttc = MetaData.getOptionalChild(mappingElement,
295: "post-table-create");
296: if (posttc != null) {
297: Iterator it = MetaData.getChildrenByTagName(posttc,
298: "sql-statement");
299: tablePostCreateCmd = new ArrayList();
300: while (it.hasNext()) {
301: Element etmp = (Element) it.next();
302: tablePostCreateCmd
303: .add(MetaData.getElementContent(etmp));
304: }
305: } else {
306: tablePostCreateCmd = defaultValues
307: .getDefaultTablePostCreateCmd();
308: }
309:
310: // alter table? If not provided, keep default.
311: String alterString = MetaData.getOptionalChildContent(
312: mappingElement, "alter-table");
313: if (alterString != null) {
314: alterTable = Boolean.valueOf(alterString).booleanValue();
315: } else {
316: alterTable = defaultValues.getAlterTable();
317: }
318:
319: // select for update
320: String sForUpString = MetaData.getOptionalChildContent(
321: mappingElement, "row-locking");
322: if (sForUpString != null) {
323: rowLocking = !isReadOnly()
324: && (Boolean.valueOf(sForUpString).booleanValue());
325: } else {
326: rowLocking = defaultValues.hasRowLocking();
327: }
328:
329: // primary key constraint? If not provided, keep default.
330: String pkString = MetaData.getOptionalChildContent(
331: mappingElement, "pk-constraint");
332: if (pkString != null) {
333: primaryKeyConstraint = Boolean.valueOf(pkString)
334: .booleanValue();
335: } else {
336: primaryKeyConstraint = defaultValues
337: .hasPrimaryKeyConstraint();
338: }
339:
340: //
341: // load metadata for each specified role
342: //
343: JDBCRelationshipRoleMetaData defaultLeft = defaultValues
344: .getLeftRelationshipRole();
345: JDBCRelationshipRoleMetaData defaultRight = defaultValues
346: .getRightRelationshipRole();
347:
348: if (!MetaData.getChildrenByTagName(element,
349: "ejb-relationship-role").hasNext()) {
350:
351: // no roles specified use the defaults
352: left = new JDBCRelationshipRoleMetaData(this ,
353: jdbcApplication, element, defaultLeft);
354:
355: right = new JDBCRelationshipRoleMetaData(this ,
356: jdbcApplication, element, defaultRight);
357:
358: left.init(right);
359: right.init(left);
360: } else {
361: Element leftElement = getEJBRelationshipRoleElement(
362: element, defaultLeft);
363: left = new JDBCRelationshipRoleMetaData(this ,
364: jdbcApplication, leftElement, defaultLeft);
365:
366: Element rightElement = getEJBRelationshipRoleElement(
367: element, defaultRight);
368: right = new JDBCRelationshipRoleMetaData(this ,
369: jdbcApplication, rightElement, defaultRight);
370:
371: left.init(right, leftElement);
372: right.init(left, rightElement);
373: }
374:
375: // at least one side of a fk relation must have keys
376: if (isForeignKeyMappingStyle() && left.getKeyFields().isEmpty()
377: && right.getKeyFields().isEmpty()) {
378: throw new DeploymentException(
379: "Atleast one role of a foreign-key "
380: + "mapped relationship must have key fields "
381: + "(or <primkey-field> is missing from ejb-jar.xml): "
382: + "ejb-relation-name=" + relationName);
383: }
384:
385: // both sides of a table relation must have keys
386: if (isTableMappingStyle()
387: && (left.getKeyFields().isEmpty() || right
388: .getKeyFields().isEmpty())) {
389: throw new DeploymentException(
390: "Both roles of a relation-table "
391: + "mapped relationship must have key fields: "
392: + "ejb-relation-name=" + relationName);
393: }
394: }
395:
396: private int loadMappingStyle(Element element,
397: JDBCRelationMetaData defaultValues)
398: throws DeploymentException {
399:
400: // if defaults check for preferred-relation-mapping
401: if ("defaults".equals(element.getTagName())) {
402: // set mapping style based on preferred-relation-mapping (if possible)
403: String perferredRelationMapping = MetaData
404: .getOptionalChildContent(element,
405: "preferred-relation-mapping");
406:
407: if ("relation-table".equals(perferredRelationMapping)
408: || defaultValues.isManyToMany()) {
409: return TABLE;
410: } else {
411: return FOREIGN_KEY;
412: }
413: }
414:
415: // check for table mapping style
416: if (MetaData
417: .getOptionalChild(element, "relation-table-mapping") != null) {
418: return TABLE;
419: }
420:
421: // check for foreign-key mapping style
422: if (MetaData.getOptionalChild(element, "foreign-key-mapping") != null) {
423: if (defaultValues.isManyToMany()) {
424: throw new DeploymentException(
425: "Foreign key mapping-style "
426: + "is not allowed for many-to-many relationsips.");
427: }
428: return FOREIGN_KEY;
429: }
430:
431: // no mapping style element, will use defaultValues
432: return defaultValues.mappingStyle;
433: }
434:
435: private static Element getMappingElement(Element element)
436: throws DeploymentException {
437:
438: // if defaults check for preferred-relation-mapping
439: if ("defaults".equals(element.getTagName())) {
440: return element;
441: }
442:
443: // check for table mapping style
444: Element tableMappingElement = MetaData.getOptionalChild(
445: element, "relation-table-mapping");
446: if (tableMappingElement != null) {
447: return tableMappingElement;
448: }
449:
450: // check for foreign-key mapping style
451: Element foreignKeyMappingElement = MetaData.getOptionalChild(
452: element, "foreign-key-mapping");
453: if (foreignKeyMappingElement != null) {
454: return foreignKeyMappingElement;
455: }
456: return null;
457: }
458:
459: private static Element getEJBRelationshipRoleElement(
460: Element element, JDBCRelationshipRoleMetaData defaultRole)
461: throws DeploymentException {
462:
463: String roleName = defaultRole.getRelationshipRoleName();
464:
465: if (roleName == null)
466: throw new DeploymentException(
467: "No ejb-relationship-role-name element found");
468:
469: Iterator iter = MetaData.getChildrenByTagName(element,
470: "ejb-relationship-role");
471: if (!iter.hasNext()) {
472: throw new DeploymentException("No ejb-relationship-role "
473: + "elements found");
474: }
475:
476: Element roleElement = null;
477: for (int i = 0; iter.hasNext(); i++) {
478: // only 2 roles are allowed
479: if (i > 1) {
480: throw new DeploymentException("Expected only 2 "
481: + "ejb-relationship-role but found more then 2");
482: }
483:
484: Element tempElement = (Element) iter.next();
485: if (roleName.equals(MetaData.getUniqueChildContent(
486: tempElement, "ejb-relationship-role-name"))) {
487: roleElement = tempElement;
488: }
489: }
490:
491: if (roleElement == null) {
492: throw new DeploymentException(
493: "An ejb-relationship-role element was "
494: + "not found for role '" + roleName + "'");
495: }
496: return roleElement;
497: }
498:
499: /**
500: * Gets the relation name. Relation name is loaded from the
501: * ejb-relation-name element.
502: *
503: * @return the name of this relation
504: */
505: public String getRelationName() {
506: return relationName;
507: }
508:
509: /**
510: * Gets the left jdbc relationship role. The relationship role is loaded
511: * from an ejb-relationship-role. Left/right assignment is completely
512: * arbitrary.
513: *
514: * @return the left JDBCRelationshipRoleMetaData
515: */
516: public JDBCRelationshipRoleMetaData getLeftRelationshipRole() {
517: return left;
518: }
519:
520: /**
521: * Gets the right jdbc relationship role. The relationship role is loaded
522: * from an ejb-relationship-role. Left/right assignment is completely
523: * arbitrary.
524: *
525: * @return the right JDBCRelationshipRoleMetaData
526: */
527: public JDBCRelationshipRoleMetaData getRightRelationshipRole() {
528: return right;
529: }
530:
531: /**
532: * Gets the relationship role related to the specified role.
533: *
534: * @param role the relationship role that the related role is desired
535: * @return the relationship role related to the specified role. right role
536: * of this relation
537: */
538: public JDBCRelationshipRoleMetaData getOtherRelationshipRole(
539: JDBCRelationshipRoleMetaData role) {
540:
541: if (left == role) {
542: return right;
543: } else if (right == role) {
544: return left;
545: } else {
546: throw new IllegalArgumentException(
547: "Specified role is not the left "
548: + "or right role. role=" + role);
549: }
550: }
551:
552: /**
553: * Should this relation be mapped to a relation table.
554: *
555: * @return true if this relation is mapped to a table
556: */
557: public boolean isTableMappingStyle() {
558: return mappingStyle == TABLE;
559: }
560:
561: /**
562: * Should this relation use foreign keys for storage.
563: *
564: * @return true if this relation is mapped to foreign keys
565: */
566: public boolean isForeignKeyMappingStyle() {
567: return mappingStyle == FOREIGN_KEY;
568: }
569:
570: /**
571: * Gets the name of the datasource in jndi for this entity
572: *
573: * @return the name of datasource in jndi
574: */
575: private String getDataSourceName() {
576: return dataSourceName;
577: }
578:
579: /**
580: * Gets the jdbc type mapping for this entity
581: *
582: * @return the jdbc type mapping for this entity
583: */
584: public JDBCTypeMappingMetaData getTypeMapping()
585: throws DeploymentException {
586: if (datasourceMapping == null) {
587: throw new DeploymentException(
588: "type-mapping is not initialized: "
589: + dataSourceName
590: + " was not deployed or type-mapping was not configured.");
591: }
592:
593: return datasourceMapping;
594: }
595:
596: /**
597: * Gets the name of the relation table.
598: *
599: * @return the name of the relation table to which is relation is mapped
600: */
601: public String getDefaultTableName() {
602: return tableName;
603: }
604:
605: /**
606: * Gets the (user-defined) SQL commands that should be issued to the db
607: * after table creation.
608: *
609: * @return the SQL command
610: */
611: public ArrayList getDefaultTablePostCreateCmd() {
612: return tablePostCreateCmd;
613: }
614:
615: /**
616: * Does the table exist yet? This does not mean that table has been created
617: * by the appilcation, or the the database metadata has been checked for the
618: * existance of the table, but that at this point the table is assumed to
619: * exist.
620: *
621: * @return true if the table exists
622: */
623: public boolean isTableCreated() {
624: return tableCreated;
625: }
626:
627: public void setTableCreated() {
628: tableCreated = true;
629: }
630:
631: /**
632: * Sets table dropped flag.
633: */
634: public void setTableDropped() {
635: this .tableDropped = true;
636: }
637:
638: public boolean isTableDropped() {
639: return tableDropped;
640: }
641:
642: /**
643: * Should the relation table be created on startup.
644: *
645: * @return true if the store mananager should attempt to create the relation
646: * table
647: */
648: public boolean getCreateTable() {
649: return createTable;
650: }
651:
652: /**
653: * Should the relation table be removed on shutdown.
654: *
655: * @return true if the store mananager should attempt to remove the relation
656: * table
657: */
658: public boolean getRemoveTable() {
659: return removeTable;
660: }
661:
662: /**
663: * Should the relation table be altered on deploy.
664: */
665: public boolean getAlterTable() {
666: return alterTable;
667: }
668:
669: /**
670: * When the relation table is created, should it have a primary key
671: * constraint.
672: *
673: * @return true if the store mananager should add a primary key constraint
674: * to the the create table sql statement
675: */
676: public boolean hasPrimaryKeyConstraint() {
677: return primaryKeyConstraint;
678: }
679:
680: /**
681: * Is this relation read-only?
682: */
683: public boolean isReadOnly() {
684: return readOnly;
685: }
686:
687: /**
688: * Gets the read time out length.
689: */
690: public int getReadTimeOut() {
691: return readTimeOut;
692: }
693:
694: /**
695: * Should select queries do row locking
696: */
697: public boolean hasRowLocking() {
698: return rowLocking;
699: }
700:
701: private String createDefaultTableName() {
702: String defaultTableName = left.getEntity().getName();
703: if (left.getCMRFieldName() != null) {
704: defaultTableName += "_" + left.getCMRFieldName();
705: }
706: defaultTableName += "_" + right.getEntity().getName();
707: if (right.getCMRFieldName() != null) {
708: defaultTableName += "_" + right.getCMRFieldName();
709: }
710: return defaultTableName;
711: }
712:
713: private boolean isManyToMany() {
714: return left.isMultiplicityMany() && right.isMultiplicityMany();
715: }
716:
717: public synchronized DataSource getDataSource() {
718: if (dataSource == null) {
719: try {
720: InitialContext context = new InitialContext();
721: dataSource = (DataSource) context
722: .lookup(dataSourceName);
723: } catch (NamingException e) {
724: throw new EJBException(
725: "Data source for relationship named "
726: + relationName + " not found "
727: + dataSourceName);
728: }
729: }
730: return dataSource;
731: }
732: }
|