0001: /*
0002: * Copyright 2004 (C) TJDO.
0003: * All rights reserved.
0004: *
0005: * This software is distributed under the terms of the TJDO License version 1.0.
0006: * See the terms of the TJDO License in the documentation provided with this software.
0007: *
0008: * $Id: StoreManager.java,v 1.18 2004/01/18 03:01:06 jackknifebarber Exp $
0009: */
0010:
0011: package com.triactive.jdo.store;
0012:
0013: import com.triactive.jdo.ClassNotPersistenceCapableException;
0014: import com.triactive.jdo.PersistenceManager;
0015: import com.triactive.jdo.PersistenceManagerFactoryImpl;
0016: import com.triactive.jdo.SchemaManager;
0017: import com.triactive.jdo.StateManager;
0018: import com.triactive.jdo.model.ClassMetaData;
0019: import com.triactive.jdo.model.FieldMetaData;
0020: import com.triactive.jdo.model.MetaData;
0021: import com.triactive.jdo.util.MacroString;
0022: import com.triactive.jdo.util.SoftValueMap;
0023: import java.sql.Connection;
0024: import java.sql.DatabaseMetaData;
0025: import java.sql.ResultSet;
0026: import java.sql.SQLException;
0027: import java.sql.SQLWarning;
0028: import java.sql.Statement;
0029: import java.util.ArrayList;
0030: import java.util.Collections;
0031: import java.util.HashMap;
0032: import java.util.HashSet;
0033: import java.util.Iterator;
0034: import java.util.List;
0035: import java.util.ListIterator;
0036: import java.util.Map;
0037: import javax.jdo.Extent;
0038: import javax.jdo.JDODataStoreException;
0039: import javax.jdo.JDOException;
0040: import javax.jdo.JDOFatalException;
0041: import javax.jdo.JDOUserException;
0042: import javax.sql.DataSource;
0043: import org.apache.log4j.Category;
0044:
0045: /**
0046: * Manages the contents of a data store (aka database schema) on behalf of a
0047: * particular PersistenceManagerFactory and all its persistent instances.
0048: * <p>
0049: * The store manager's responsibilities include:
0050: * <ul>
0051: * <li>Creating and/or validating database tables according to the persistent
0052: * classes being accessed by the application.</li>
0053: * <li>Serving as the primary intermediary between StateManagers and the
0054: * database (implements insert(), fetch(), update(), delete()).</li>
0055: * <li>Managing TJDO's schema table (JDO_TABLE).
0056: * <li>Serving as the base Extent and Query factory.</li>
0057: * <li>Providing cached access to JDBC database metadata.</li>
0058: * <li>Resolving SQL identifier macros to actual SQL identifiers.</li>
0059: * </ul>
0060: * <p>
0061: * A store manager's knowledge of its schema's contents is not necessarily
0062: * complete.
0063: * It is aware of only those tables whose classes have somehow been accessed,
0064: * directly or indirectly, by the application during the life of the store
0065: * manager object.
0066: *
0067: * @author <a href="mailto:mmartin5@austin.rr.com">Mike Martin</a>
0068: * @version $Revision: 1.18 $
0069: */
0070:
0071: public class StoreManager implements SchemaManager {
0072: private static final Category LOG = Category
0073: .getInstance(StoreManager.class);
0074:
0075: /**
0076: * The amount of time before we expire any cached column info. Hardcoded to
0077: * five minutes.
0078: */
0079: private static final int COLUMN_INFO_EXPIRATION_MS = 5 * 60 * 1000;
0080:
0081: private final DataSource ds;
0082: private final String userName;
0083: private final String password;
0084: private final DatabaseAdapter dba;
0085: private final int tableValidationFlags;
0086: private final int constraintValidationFlags;
0087: private final String schemaName;
0088:
0089: /*
0090: * Access to fields below is synchronized on the StoreManager object.
0091: * Note: if DB work must also be done, the DB connection must be
0092: * acquired *before* locking the StoreManager in order to avoid deadlocks.
0093: */
0094: private ArrayList allTables = new ArrayList();
0095: private ArrayList tablesByTableID = new ArrayList();
0096: private HashMap tablesByName = new HashMap();
0097: private HashMap tablesByJavaID = new HashMap();
0098: private SchemaTable schemaTable = null;
0099: private Map columnInfoByTableName = new HashMap();
0100: private long columnInfoReadTimestamp = -1;
0101:
0102: /**
0103: * The cache of database requests. Access is synchronized on the map
0104: * object itself.
0105: */
0106: private Map requestsByID = Collections
0107: .synchronizedMap(new SoftValueMap());
0108:
0109: /**
0110: * The active class adder transaction, if any.
0111: * Some StoreManager methods are called recursively in the course of adding
0112: * new classes.
0113: * This field allows such methods to coordinate with the active ClassAdder
0114: * transaction.
0115: * Recursive methods include:
0116: * <ul>
0117: * <li>addClasses()</li>
0118: * <li>newSetTable()</li>
0119: * <li>newMapTable()</li>
0120: * </ul>
0121: * Access is synchronized on the StoreManager itself.
0122: * Invariant: classAdder == null if StoreManager is unlocked.
0123: */
0124: private ClassAdder classAdder = null;
0125:
0126: /**
0127: * Constructs a new StoreManager.
0128: * On successful return the new StoreManager will have successfully
0129: * connected to the database with the given credentials and determined the
0130: * schema name, but will not have inspected the schema contents any further.
0131: * The contents (tables, views, etc.) will be subsequently created and/or
0132: * validated on-demand as the application accesses persistent classes.
0133: * <p>
0134: * To avoid creating unnecessary redundant StoreManagers, new StoreManagers
0135: * should always be obtained from the {@link StoreManagerFactory}, rather
0136: * than constructed directly.
0137: *
0138: * @param pmf
0139: * The corresponding PersistenceManagerFactory. This factory's
0140: * non-transactional data source will be used to get database
0141: * connections as needed to perform management functions.
0142: * @param userName
0143: * The database user name.
0144: * @param password
0145: * The database user's password.
0146: *
0147: * @exception JDODataStoreException
0148: * If the database could not be accessed or the name of the schema
0149: * could not be determined.
0150: *
0151: * @see StoreManagerFactory
0152: */
0153:
0154: StoreManager(PersistenceManagerFactoryImpl pmf, String userName,
0155: String password) {
0156: this .ds = pmf.getNontransactionalDataSource();
0157: this .userName = userName;
0158: this .password = password;
0159:
0160: int validateTables = pmf.getValidateTables() ? Table.VALIDATE
0161: : 0;
0162: int validateConstraints = pmf.getValidateConstraints() ? Table.VALIDATE
0163: : 0;
0164: int autoCreate = pmf.getAutoCreateTables() ? Table.AUTO_CREATE
0165: : 0;
0166:
0167: tableValidationFlags = validateTables | autoCreate;
0168: constraintValidationFlags = validateConstraints | autoCreate;
0169:
0170: try {
0171: Connection conn;
0172:
0173: if (userName == null)
0174: conn = ds.getConnection();
0175: else
0176: conn = ds.getConnection(userName, password);
0177:
0178: try {
0179: dba = DatabaseAdapter.getInstance(conn);
0180: String name;
0181:
0182: try {
0183: name = dba.getSchemaName(conn);
0184: } catch (UnsupportedOperationException e) {
0185: /*
0186: * The DatabaseAdapter doesn't know how to determine the
0187: * current schema name. As a fallback, create a temporary
0188: * probe table which can look itself up in the JDBC metadata
0189: * and thereby find out what schema we're in.
0190: */
0191: ProbeTable pt = new ProbeTable(this );
0192: pt.initialize();
0193: pt.create(conn);
0194:
0195: try {
0196: name = pt.findSchemaName(conn);
0197: } finally {
0198: pt.drop(conn);
0199: }
0200: }
0201:
0202: schemaName = name;
0203: } finally {
0204: conn.close();
0205: }
0206: } catch (SQLException e) {
0207: throw new JDODataStoreException(
0208: "Failed initializing database", e);
0209: }
0210: }
0211:
0212: /**
0213: * Clears all knowledge of tables, cached requests, metadata, etc and resets
0214: * the store manager to its initial state.
0215: */
0216:
0217: public synchronized void reset() {
0218: allTables.clear();
0219: tablesByTableID.clear();
0220: tablesByName.clear();
0221: tablesByJavaID.clear();
0222: schemaTable = null;
0223:
0224: columnInfoByTableName.clear();
0225: columnInfoReadTimestamp = -1;
0226:
0227: requestsByID.clear();
0228: }
0229:
0230: /**
0231: * Asserts that the schema has been initialized, meaning the schema table
0232: * (JDO_TABLE) exists and is valid, and the schemaTable field is non-null.
0233: * <p>
0234: * This method must be called with the StoreManager's monitor unlocked,
0235: * since it may invoke a management transaction.
0236: */
0237:
0238: private void checkSchemaInitialized() {
0239: //assert !Thread.holdsLock(this); // only possible with 1.4
0240:
0241: synchronized (this ) {
0242: if (schemaTable != null)
0243: return;
0244: }
0245:
0246: MgmtTransaction mtx = new MgmtTransaction(
0247: Connection.TRANSACTION_SERIALIZABLE) {
0248: public String toString() {
0249: return "Initialize schema table for " + schemaName;
0250: }
0251:
0252: protected void execute(Connection conn) throws SQLException {
0253: initializeSchemaTable(true, conn);
0254: }
0255: };
0256:
0257: mtx.execute();
0258: }
0259:
0260: /**
0261: * Initializes the schemaTable field.
0262: *
0263: * @param validate
0264: * <code>true</code> to validate the table even if it doesn't exist.
0265: * If auto-create mode is on this will cause it to be created.
0266: * @param conn
0267: * The connection to use.
0268: *
0269: * @return
0270: * <code>true</code> if the schemaTable field has been set or was
0271: * already set,
0272: * <code>false</code> if validation is false and the table doesn't
0273: * exist.
0274: */
0275: private synchronized boolean initializeSchemaTable(
0276: boolean validate, Connection conn) throws SQLException {
0277: if (schemaTable != null)
0278: return true;
0279:
0280: try {
0281: SchemaTable st = new SchemaTable(this );
0282: st.initialize();
0283:
0284: if (validate || st.exists(conn)) {
0285: st.validate(tableValidationFlags, conn);
0286:
0287: LOG.info("Schema initialized: " + schemaName);
0288:
0289: schemaTable = st;
0290: return true;
0291: } else
0292: return false;
0293: } finally {
0294: if (schemaTable == null)
0295: reset();
0296: }
0297: }
0298:
0299: /**
0300: * Returns the database adapter used by this store manager.
0301: *
0302: * @return The database adapter used by this store manager.
0303: */
0304:
0305: public DatabaseAdapter getDatabaseAdapter() {
0306: return dba;
0307: }
0308:
0309: public String getSchemaName() {
0310: return schemaName;
0311: }
0312:
0313: /**
0314: * Logs SQL warnings to the common log. Should be called after any
0315: * operation on a JDBC <tt>Statement</tt> or <tt>ResultSet</tt>
0316: * object as:
0317: *
0318: * <blockquote><pre>
0319: * storeMgr.logSQLWarnings(obj.getWarnings());
0320: * </pre></blockquote>
0321: *
0322: * If any warnings were generated, the entire list of them are logged
0323: * to <tt>System.err</tt>.
0324: *
0325: * @param warning the value returned from getWarnings().
0326: */
0327:
0328: public void logSQLWarnings(SQLWarning warning) {
0329: while (warning != null) {
0330: LOG.warn("SQL warning: " + warning);
0331:
0332: warning = warning.getNextWarning();
0333: }
0334: }
0335:
0336: public void logSQLWarnings(Connection conn) {
0337: try {
0338: logSQLWarnings(conn.getWarnings());
0339: } catch (SQLException e) {
0340: throw dba.newDataStoreException(
0341: "Error obtaining warnings from connection " + conn,
0342: e);
0343: }
0344: }
0345:
0346: public void logSQLWarnings(Statement stmt) {
0347: try {
0348: logSQLWarnings(stmt.getWarnings());
0349: } catch (SQLException e) {
0350: throw dba.newDataStoreException(
0351: "Error obtaining warnings from statement " + stmt,
0352: e);
0353: }
0354: }
0355:
0356: public void logSQLWarnings(ResultSet rs) {
0357: try {
0358: logSQLWarnings(rs.getWarnings());
0359: } catch (SQLException e) {
0360: throw dba
0361: .newDataStoreException(
0362: "Error obtaining warnings from result set "
0363: + rs, e);
0364: }
0365: }
0366:
0367: /**
0368: * Adds the given persistence-capable classes to the store manager's set
0369: * of active classes ready for persistence.
0370: * The act of adding a class to the store manager causes any necessary
0371: * database objects (tables, views, constraints, indexes, etc.) to be
0372: * created.
0373: * <p>
0374: * Other StoreManager methods may cause classes to be added as a side
0375: * effect.
0376: *
0377: * @param classes The class(es) to be added.
0378: */
0379:
0380: public void addClasses(Class[] classes) {
0381: checkSchemaInitialized();
0382:
0383: synchronized (this ) {
0384: if (classAdder != null) {
0385: /*
0386: * addClasses() has been recursively re-entered: just add table
0387: * objects for the requested classes and return.
0388: */
0389: classAdder.addClasses(classes);
0390: return;
0391: }
0392: }
0393:
0394: new ClassAdder(classes).execute();
0395: }
0396:
0397: /**
0398: * Called by Mapping objects in the midst of StoreManager.addClasses()
0399: * to request the creation of a set table.
0400: *
0401: * @param cbt The base table for the class containing the set.
0402: * @param fmd The field metadata describing the set field.
0403: */
0404:
0405: synchronized SetTable newSetTable(ClassBaseTable cbt,
0406: FieldMetaData fmd) {
0407: if (classAdder == null)
0408: throw new IllegalStateException(
0409: "SetTables can only be created as a side effect of adding a new class");
0410:
0411: return classAdder.newSetTable(cbt, fmd);
0412: }
0413:
0414: /**
0415: * Called by Mapping objects in the midst of StoreManager.addClasses()
0416: * to request the creation of a map table.
0417: *
0418: * @param cbt The base table for the class containing the map.
0419: * @param fmd The field metadata describing the map field.
0420: */
0421:
0422: synchronized MapTable newMapTable(ClassBaseTable cbt,
0423: FieldMetaData fmd) {
0424: if (classAdder == null)
0425: throw new IllegalStateException(
0426: "MapTables can only be created as a side effect of adding a new class");
0427:
0428: return classAdder.newMapTable(cbt, fmd);
0429: }
0430:
0431: public void dropTablesFor(final Class[] classes) {
0432: MgmtTransaction mtx = new MgmtTransaction(
0433: Connection.TRANSACTION_READ_COMMITTED) {
0434: public String toString() {
0435: return "Drop tables for selected classes from schema "
0436: + schemaName;
0437: }
0438:
0439: protected void execute(Connection conn) throws SQLException {
0440: synchronized (StoreManager.this ) {
0441: try {
0442: if (initializeSchemaTable(false, conn))
0443: schemaTable.dropTablesFor(classes, conn);
0444: } finally {
0445: reset();
0446: }
0447: }
0448: }
0449: };
0450:
0451: mtx.execute();
0452: }
0453:
0454: public void dropAllTables() {
0455: MgmtTransaction mtx = new MgmtTransaction(
0456: Connection.TRANSACTION_READ_COMMITTED) {
0457: public String toString() {
0458: return "Drop all tables from schema " + schemaName;
0459: }
0460:
0461: protected void execute(Connection conn) throws SQLException {
0462: synchronized (StoreManager.this ) {
0463: try {
0464: if (initializeSchemaTable(false, conn)) {
0465: schemaTable.dropAllTables(conn);
0466: schemaTable.drop(conn);
0467: }
0468: } finally {
0469: reset();
0470: }
0471: }
0472: }
0473: };
0474:
0475: mtx.execute();
0476: }
0477:
0478: /**
0479: * Returns the JDO table having the given SQL name, if any.
0480: * Returns <code>null</code> if no such table is (yet) known to the store
0481: * manager.
0482: *
0483: * @param name The name of the table.
0484: *
0485: * @return The corresponding JDO table, or <code>null</code>.
0486: */
0487:
0488: public synchronized JDOTable getTable(SQLIdentifier name) {
0489: return (JDOTable) tablesByName.get(name);
0490: }
0491:
0492: /**
0493: * Returns the JDO table having the given table ID.
0494: * Returns <code>null</code> if no such table is (yet) known to the store
0495: * manager.
0496: *
0497: * @param tableID
0498: * The table ID of the table to be returned.
0499: *
0500: * @return The corresponding JDO table, or <code>null</code>.
0501: */
0502:
0503: public synchronized JDOTable getTable(int tableID) {
0504: if (tableID < 0 || tableID >= tablesByTableID.size())
0505: return null;
0506: else
0507: return (JDOTable) tablesByTableID.get(tableID);
0508: }
0509:
0510: /**
0511: * Returns the JDO table having the given metadata, if any.
0512: * Returns <code>null</code> if no such table is (yet) known to the store
0513: * manager.
0514: *
0515: * @param md The metadata for the table.
0516: *
0517: * @return The corresponding JDO table, or <code>null</code>.
0518: */
0519:
0520: synchronized JDOTable getTable(MetaData md) {
0521: return (JDOTable) tablesByJavaID.get(md.getJavaName());
0522: }
0523:
0524: /**
0525: * Returns the primary table serving as backing for the given class.
0526: * If the class is not yet known to the store manager, {@link #addClasses}
0527: * is called to add it.
0528: *
0529: * @param c The class whose table is be returned.
0530: *
0531: * @return The corresponding class table.
0532: *
0533: * @exception NoExtentException
0534: * If the given class is not persistence-capable.
0535: */
0536:
0537: public ClassTable getTable(Class c) {
0538: ClassTable ct;
0539:
0540: synchronized (this ) {
0541: ct = (ClassTable) tablesByJavaID.get(c.getName());
0542: }
0543:
0544: if (ct == null) {
0545: addClasses(new Class[] { c });
0546:
0547: /* Retry. */
0548: synchronized (this ) {
0549: ct = (ClassTable) tablesByJavaID.get(c.getName());
0550: }
0551:
0552: if (ct == null)
0553: throw new NoExtentException(c);
0554: }
0555:
0556: return ct;
0557: }
0558:
0559: /**
0560: * Returns the primary table serving as backing for the given class.
0561: * If the class is not yet known to the store manager, {@link #addClasses}
0562: * is called to add it.
0563: * <p>
0564: * This method is functionally equivalent to {@link #getTable(Class)}
0565: * except that it will only return base tables (not views).
0566: *
0567: * @param c The class whose table is be returned.
0568: *
0569: * @return The corresponding class table.
0570: *
0571: * @exception NoExtentException
0572: * If the given class is not persistence-capable.
0573: * @exception ViewNotSupportedException
0574: * If the given class is backed by a view.
0575: */
0576:
0577: public ClassBaseTable getClassBaseTable(Class c) {
0578: ClassTable t = getTable(c);
0579:
0580: if (!(t instanceof ClassBaseTable))
0581: throw new ViewNotSupportedException(c); // must be a ClassView
0582:
0583: return (ClassBaseTable) t;
0584: }
0585:
0586: /**
0587: * Returns the Java name of the JDO table having the given table ID.
0588: * Returns <code>null</code> if no such table exists in the schema.
0589: * <p>
0590: * If the table is not (yet) known to the store manager, this method does
0591: * <em>not</em> initialize it.
0592: *
0593: * @param tableID
0594: * The table ID of the table to be returned.
0595: *
0596: * @return The corresponding Java name, or <code>null</code>.
0597: */
0598:
0599: public String getJavaName(final int tableID) {
0600: JDOTable table = getTable(tableID);
0601:
0602: if (table != null)
0603: return table.getJavaName();
0604: else {
0605: checkSchemaInitialized();
0606:
0607: final String s[] = new String[1];
0608:
0609: MgmtTransaction mtx = new MgmtTransaction(
0610: Connection.TRANSACTION_READ_COMMITTED) {
0611: public String toString() {
0612: return "Query schema table for schema "
0613: + schemaName;
0614: }
0615:
0616: protected void execute(Connection conn)
0617: throws SQLException {
0618: synchronized (StoreManager.this ) {
0619: if (schemaTable != null)
0620: s[0] = schemaTable.getJavaName(tableID,
0621: conn);
0622: }
0623: }
0624: };
0625:
0626: mtx.execute();
0627:
0628: String javaName = s[0];
0629:
0630: if (javaName == null)
0631: throw new JDOUserException("Unknown table ID = "
0632: + tableID);
0633:
0634: return javaName;
0635: }
0636: }
0637:
0638: public Extent getExtent(PersistenceManager pm, Class c,
0639: boolean subclasses) {
0640: ClassTable t = getTable(c);
0641:
0642: return t.newExtent(pm, subclasses);
0643: }
0644:
0645: public Query getQuery(PersistenceManager pm, Object query) {
0646: return getQuery("javax.jdo.query.JDOQL", pm, query);
0647: }
0648:
0649: public Query getQuery(String language, PersistenceManager pm,
0650: Object query) {
0651: Query q;
0652:
0653: if (language.equals("javax.jdo.query.JDOQL")) {
0654: if (query != null && !(query instanceof JDOQLQuery))
0655: throw new JDOUserException("Invalid query argument, "
0656: + query + ", should be an object of type "
0657: + JDOQLQuery.class.getName());
0658:
0659: q = new JDOQLQuery(pm, this , (JDOQLQuery) query);
0660: } else if (language.equals("javax.jdo.query.TJDOSQL")) {
0661: if (query == null || !(query instanceof String))
0662: throw new JDOUserException(
0663: "Invalid query argument, "
0664: + query
0665: + ", should be a String containing a SQL SELECT statement, optionally using embedded macros");
0666:
0667: q = new TJDOSQLQuery(pm, this , (String) query);
0668: } else
0669: throw new JDOUserException("Unknown query language: "
0670: + language);
0671:
0672: return q;
0673: }
0674:
0675: /**
0676: * Returns a new, unique ID for an object of the given class.
0677: *
0678: * @param c The class of the object.
0679: *
0680: * @return A new object ID.
0681: */
0682:
0683: public Object newObjectID(Class c) {
0684: ClassMetaData cmd = ClassMetaData.forClass(c);
0685:
0686: if (cmd == null)
0687: throw new ClassNotPersistenceCapableException(c);
0688:
0689: if (cmd.requiresExtent())
0690: return getTable(c).newOID();
0691: else
0692: return new SCOID(c);
0693: }
0694:
0695: /**
0696: * Returns the next OID high-order value for IDs of the given class.
0697: *
0698: * @param classID The class ID number of the class.
0699: *
0700: * @return The next high-order OID value.
0701: *
0702: * @exception JDODataStoreException
0703: * If an error occurs in accessing or updating the schema table.
0704: */
0705:
0706: int getNextOIDHiValue(final int classID) {
0707: final int[] nextHiValue = new int[] { -1 };
0708: MgmtTransaction mtx = new MgmtTransaction(
0709: Connection.TRANSACTION_SERIALIZABLE) {
0710: public String toString() {
0711: return "Obtain next ID value for class ID " + classID;
0712: }
0713:
0714: protected void execute(Connection conn) throws SQLException {
0715: nextHiValue[0] = schemaTable.getNextOIDHiValue(classID,
0716: conn);
0717: }
0718: };
0719:
0720: mtx.execute();
0721:
0722: return nextHiValue[0];
0723: }
0724:
0725: /**
0726: * Inserts a persistent object into the database.
0727: *
0728: * @param sm The state manager of the object to be inserted.
0729: */
0730:
0731: public void insert(StateManager sm) {
0732: getClassBaseTable(sm.getObject().getClass()).insert(sm);
0733: }
0734:
0735: /**
0736: * Confirms that a persistent object exists in the database.
0737: *
0738: * @param sm The state manager of the object to be fetched.
0739: */
0740:
0741: public void lookup(StateManager sm) {
0742: getClassBaseTable(sm.getObject().getClass()).lookup(sm);
0743: }
0744:
0745: /**
0746: * Fetches a persistent object from the database.
0747: *
0748: * @param sm The state manager of the object to be fetched.
0749: * @param fieldNumbers The numbers of the fields to be fetched.
0750: */
0751:
0752: public void fetch(StateManager sm, int fieldNumbers[]) {
0753: getClassBaseTable(sm.getObject().getClass()).fetch(sm,
0754: fieldNumbers);
0755: }
0756:
0757: /**
0758: * Updates a persistent object in the database.
0759: *
0760: * @param sm The state manager of the object to be updated.
0761: * @param fieldNumbers The numbers of the fields to be updated.
0762: */
0763:
0764: public void update(StateManager sm, int fieldNumbers[]) {
0765: getClassBaseTable(sm.getObject().getClass()).update(sm,
0766: fieldNumbers);
0767: }
0768:
0769: /**
0770: * Deletes a persistent object from the database.
0771: *
0772: * @param sm The state manager of the object to be deleted.
0773: */
0774:
0775: public void delete(StateManager sm) {
0776: getClassBaseTable(sm.getObject().getClass()).delete(sm);
0777: }
0778:
0779: /**
0780: * Returns a request object that will insert a row in the given table.
0781: * The store manager will cache the request object for re-use by subsequent
0782: * requests to the same table.
0783: *
0784: * @param cbt The table into which to insert.
0785: *
0786: * @return An insertion request object.
0787: */
0788:
0789: InsertRequest getInsertRequest(ClassBaseTable cbt) {
0790: RequestIdentifier reqID = new RequestIdentifier(cbt, null,
0791: RequestIdentifier.Type.INSERT);
0792: InsertRequest req;
0793:
0794: req = (InsertRequest) requestsByID.get(reqID);
0795:
0796: if (req == null) {
0797: req = new InsertRequest(cbt);
0798: requestsByID.put(reqID, req);
0799: }
0800:
0801: return req;
0802: }
0803:
0804: /**
0805: * Returns a request object that will lookup a row in the given table.
0806: * The store manager will cache the request object for re-use by subsequent
0807: * requests to the same table.
0808: *
0809: * @param cbt The table in which to lookup.
0810: *
0811: * @return A lookup request object.
0812: */
0813:
0814: LookupRequest getLookupRequest(ClassBaseTable cbt) {
0815: RequestIdentifier reqID = new RequestIdentifier(cbt, null,
0816: RequestIdentifier.Type.LOOKUP);
0817: LookupRequest req;
0818:
0819: req = (LookupRequest) requestsByID.get(reqID);
0820:
0821: if (req == null) {
0822: req = new LookupRequest(cbt);
0823: requestsByID.put(reqID, req);
0824: }
0825:
0826: return req;
0827: }
0828:
0829: /**
0830: * Returns a request object that will fetch a row from the given table.
0831: * The store manager will cache the request object for re-use by subsequent
0832: * requests to the same table.
0833: *
0834: * @param cbt The table from which to fetch.
0835: * @param fieldNumbers The field numbers corresponding to the columns to be
0836: * fetched. Field numbers whose columns exist in
0837: * supertables will be ignored.
0838: *
0839: * @return A fetch request object.
0840: */
0841:
0842: FetchRequest getFetchRequest(ClassBaseTable cbt, int[] fieldNumbers) {
0843: RequestIdentifier reqID = new RequestIdentifier(cbt,
0844: fieldNumbers, RequestIdentifier.Type.FETCH);
0845: FetchRequest req;
0846:
0847: req = (FetchRequest) requestsByID.get(reqID);
0848:
0849: if (req == null) {
0850: req = new FetchRequest(cbt, fieldNumbers);
0851: requestsByID.put(reqID, req);
0852: }
0853:
0854: return req;
0855: }
0856:
0857: /**
0858: * Returns a request object that will update a row in the given table.
0859: * The store manager will cache the request object for re-use by subsequent
0860: * requests to the same table.
0861: *
0862: * @param cbt The table in which to update.
0863: * @param fieldNumbers The field numbers corresponding to the columns to be
0864: * updated. Field numbers whose columns exist in
0865: * supertables will be ignored.
0866: *
0867: * @return An update request object.
0868: */
0869:
0870: UpdateRequest getUpdateRequest(ClassBaseTable cbt,
0871: int[] fieldNumbers) {
0872: RequestIdentifier reqID = new RequestIdentifier(cbt,
0873: fieldNumbers, RequestIdentifier.Type.UPDATE);
0874: UpdateRequest req;
0875:
0876: req = (UpdateRequest) requestsByID.get(reqID);
0877:
0878: if (req == null) {
0879: req = new UpdateRequest(cbt, fieldNumbers);
0880: requestsByID.put(reqID, req);
0881: }
0882:
0883: return req;
0884: }
0885:
0886: /**
0887: * Returns a request object that will delete a row from the given table.
0888: * The store manager will cache the request object for re-use by subsequent
0889: * requests to the same table.
0890: *
0891: * @param cbt The table from which to delete.
0892: *
0893: * @return A deletion request object.
0894: */
0895:
0896: DeleteRequest getDeleteRequest(ClassBaseTable cbt) {
0897: RequestIdentifier reqID = new RequestIdentifier(cbt, null,
0898: RequestIdentifier.Type.DELETE);
0899: DeleteRequest req;
0900:
0901: req = (DeleteRequest) requestsByID.get(reqID);
0902:
0903: if (req == null) {
0904: req = new DeleteRequest(cbt);
0905: requestsByID.put(reqID, req);
0906: }
0907:
0908: return req;
0909: }
0910:
0911: /**
0912: * Returns the type of a database table.
0913: *
0914: * @param tableName The name of the table (or view).
0915: * @param conn A JDBC connection to the database.
0916: *
0917: * @return one of the TABLE_TYPE_* values from {@link Table}.
0918: *
0919: * @see Table
0920: */
0921:
0922: public int getTableType(SQLIdentifier tableName, Connection conn)
0923: throws SQLException {
0924: String tableType = null;
0925: String tableNameSQL = tableName.getSQLIdentifier();
0926: DatabaseMetaData dmd = conn.getMetaData();
0927:
0928: ResultSet rs = dmd.getTables(null, schemaName, tableNameSQL,
0929: null);
0930:
0931: try {
0932: while (rs.next()) {
0933: if (tableNameSQL.equalsIgnoreCase(rs.getString(3))) {
0934: tableType = rs.getString(4).toUpperCase();
0935: break;
0936: }
0937: }
0938: } finally {
0939: rs.close();
0940: }
0941:
0942: if (tableType == null)
0943: return Table.TABLE_TYPE_MISSING;
0944: else if (tableType.equals("TABLE"))
0945: return Table.TABLE_TYPE_BASE_TABLE;
0946: else if (tableType.equals("VIEW"))
0947: return Table.TABLE_TYPE_VIEW;
0948: else
0949: return Table.TABLE_TYPE_UNKNOWN;
0950: }
0951:
0952: /**
0953: * Returns the column info for a database table. This should be used
0954: * instead of making direct calls to DatabaseMetaData.getColumns().
0955: *
0956: * <p>Where possible, this method loads and caches column info for more than
0957: * just the table being requested, improving performance by reducing the
0958: * overall number of calls made to DatabaseMetaData.getColumns() (each of
0959: * which usually results in one or more database queries).
0960: *
0961: * @param tableName The name of the table (or view).
0962: * @param conn A JDBC connection to the database.
0963: *
0964: * @return A list of ColumnInfo objects describing the columns of the
0965: * table. The list is in the same order as was supplied by
0966: * getColumns(). If no column info is found for the given table,
0967: * an empty list is returned.
0968: *
0969: * @see ColumnInfo
0970: */
0971:
0972: List getColumnInfo(SQLIdentifier tableName, Connection conn)
0973: throws SQLException {
0974: List cols = null;
0975:
0976: if (schemaTable == null) {
0977: /*
0978: * There's no SchemaTable yet. We can't yet employ the smart
0979: * caching stuff below, so we just make a direct JDBC metadata
0980: * query to get the info for this one table. This should only be
0981: * the case when we're validating the SchemaTable itself.
0982: */
0983: cols = new ArrayList();
0984: DatabaseMetaData dmd = conn.getMetaData();
0985: ResultSet rs = dmd.getColumns(null, schemaName, tableName
0986: .getSQLIdentifier(), null);
0987:
0988: try {
0989: while (rs.next())
0990: cols.add(dba.newColumnInfo(rs));
0991: } finally {
0992: rs.close();
0993: }
0994: } else {
0995: synchronized (this ) {
0996: long now = System.currentTimeMillis();
0997:
0998: /*
0999: * If we have cached column info that hasn't expired yet, see
1000: * if it contains info for the given table.
1001: */
1002: if (now >= columnInfoReadTimestamp
1003: && now < columnInfoReadTimestamp
1004: + COLUMN_INFO_EXPIRATION_MS)
1005: cols = (List) columnInfoByTableName.get(tableName);
1006:
1007: /*
1008: * If we have no info for that table, or stale info overall,
1009: * refresh the column info cache.
1010: */
1011: if (cols == null) {
1012: /*
1013: * Determine the set of known JDO tables according to what's
1014: * recorded in the schema table.
1015: */
1016: HashSet knownTableNames = new HashSet();
1017: Iterator i = schemaTable.getAllTableMetadata(false,
1018: conn).iterator();
1019:
1020: while (i.hasNext())
1021: knownTableNames
1022: .add(((TableMetadata) i.next()).tableName);
1023:
1024: /*
1025: * Query for column info on all tables in the schema,
1026: * discarding info for any tables that aren't JDO tables,
1027: * or that are JDO tables but are already validated.
1028: */
1029: HashMap cim = new HashMap();
1030: DatabaseMetaData dmd = conn.getMetaData();
1031: ResultSet rs = dmd.getColumns(null, schemaName,
1032: null, null);
1033:
1034: try {
1035: while (rs.next()) {
1036: SQLIdentifier tblName = new SQLIdentifier(
1037: dba, rs.getString(3));
1038:
1039: if (knownTableNames.contains(tblName)) {
1040: Table tbl = (Table) tablesByName
1041: .get(tblName);
1042:
1043: if (tbl == null || !tbl.isValidated()) {
1044: List l = (List) cim.get(tblName);
1045:
1046: if (l == null) {
1047: l = new ArrayList();
1048: cim.put(tblName, l);
1049: }
1050:
1051: l.add(dba.newColumnInfo(rs));
1052: }
1053: }
1054: }
1055: } finally {
1056: rs.close();
1057: }
1058:
1059: if (LOG.isDebugEnabled())
1060: LOG.debug("Column info loaded for "
1061: + schemaName + ", " + cim.size()
1062: + " tables, time = "
1063: + (System.currentTimeMillis() - now)
1064: + " ms");
1065:
1066: /* Replace the old cache (if any) with the new one. */
1067: columnInfoByTableName = cim;
1068: columnInfoReadTimestamp = now;
1069:
1070: /*
1071: * Finally, lookup info for the desired table in the new
1072: * cache.
1073: */
1074: cols = (List) columnInfoByTableName.get(tableName);
1075:
1076: if (cols == null) {
1077: cols = Collections.EMPTY_LIST;
1078: LOG.warn("No column info found for "
1079: + tableName);
1080: }
1081: }
1082: }
1083: }
1084:
1085: return cols;
1086: }
1087:
1088: /**
1089: * Returns the foreign key info for a database table.
1090: * This should be used instead of making direct calls to
1091: * DatabaseMetaData.getImportedKeys() or DatabaseMetaData.getExportedKeys().
1092: *
1093: * @param tableName The name of the table (or view).
1094: * @param conn A JDBC connection to the database.
1095: *
1096: * @return
1097: * A list of ForeignKeyInfo objects describing the columns of the
1098: * table's foreign keys.
1099: * The list is in the same order as was supplied by get??portedKeys().
1100: * If no column info is found for the given table, an empty list is
1101: * returned.
1102: *
1103: * @see ForeignKeyInfo
1104: */
1105:
1106: List getForeignKeyInfo(SQLIdentifier tableName, Connection conn)
1107: throws SQLException {
1108: List fkCols = new ArrayList();
1109: DatabaseMetaData dmd = conn.getMetaData();
1110: ResultSet rs = dmd.getImportedKeys(null, schemaName, tableName
1111: .getSQLIdentifier());
1112:
1113: try {
1114: while (rs.next()) {
1115: ForeignKeyInfo fki = dba.newForeignKeyInfo(rs);
1116:
1117: /*
1118: * The contains() test is necessary only because some drivers
1119: * have been known to be so confused (PostgreSQL, I'm looking at
1120: * you) as to return duplicate rows from getImportedKeys().
1121: */
1122: if (!fkCols.contains(fki))
1123: fkCols.add(fki);
1124: }
1125: } finally {
1126: rs.close();
1127: }
1128:
1129: return fkCols;
1130: }
1131:
1132: /**
1133: * Tests if a database table exists.
1134: *
1135: * @param tableName The name of the table (or view).
1136: * @param conn A JDBC connection to the database.
1137: *
1138: * @return <tt>true</tt> if the table exists in the database,
1139: * <tt>false</tt> otherwise.
1140: */
1141:
1142: public boolean tableExists(SQLIdentifier tableName, Connection conn)
1143: throws SQLException {
1144: return getTableType(tableName, conn) != Table.TABLE_TYPE_MISSING;
1145: }
1146:
1147: /**
1148: * Resolves an identifier macro. The public fields <var>clazz</var>,
1149: * <var>fieldName</var>, and <var>subfieldName</var> of the given macro are
1150: * taken as inputs, and the public <var>value</var> field is set to the SQL
1151: * identifier of the corresponding database table or column.
1152: *
1153: * @param im The macro to resolve.
1154: */
1155:
1156: public void resolveIdentifierMacro(MacroString.IdentifierMacro im) {
1157: ClassTable ct = getTable(im.clazz);
1158:
1159: if (im.fieldName == null) {
1160: im.value = ct.getName().toString();
1161: return;
1162: }
1163:
1164: ColumnMapping cm;
1165:
1166: if (im.fieldName.equals("this")) {
1167: if (!(ct instanceof ClassBaseTable))
1168: throw new JDOUserException("Table for class "
1169: + im.clazz.getName() + " has no ID column");
1170:
1171: if (im.subfieldName != null)
1172: throw new JDOUserException(
1173: "Field "
1174: + im.clazz.getName()
1175: + ".this has no table of its own in which to look for "
1176: + im.subfieldName);
1177:
1178: cm = ((ClassBaseTable) ct).getIDMapping();
1179: } else {
1180: ClassMetaData cmd = ClassMetaData.forClass(im.clazz);
1181: FieldMetaData fmd = cmd.getFieldRelative(cmd
1182: .getRelativeFieldNumber(im.fieldName));
1183:
1184: if (im.subfieldName == null) {
1185: Mapping m = ct.getFieldMapping(im.fieldName);
1186:
1187: if (m instanceof ColumnMapping)
1188: cm = (ColumnMapping) m;
1189: else {
1190: JDOTable t = getTable(fmd);
1191:
1192: if (t == null)
1193: throw new JDOUserException(
1194: "Invalid pseudo-field name "
1195: + im.subfieldName
1196: + " in macro "
1197: + im
1198: + ", has no backing table or column");
1199:
1200: im.value = t.getName().toString();
1201: return;
1202: }
1203: } else {
1204: JDOTable t = getTable(fmd);
1205:
1206: if (t instanceof SetTable) {
1207: SetTable st = (SetTable) t;
1208:
1209: if (im.subfieldName.equals("owner"))
1210: cm = st.getOwnerMapping();
1211: else if (im.subfieldName.equals("element"))
1212: cm = st.getElementMapping();
1213: else
1214: throw new JDOUserException(
1215: "Invalid pseudo-field name "
1216: + im.subfieldName
1217: + " in macro "
1218: + im
1219: + ", must be \"owner\" or \"element\"");
1220: } else if (t instanceof MapTable) {
1221: MapTable mt = (MapTable) t;
1222:
1223: if (im.subfieldName.equals("owner"))
1224: cm = mt.getOwnerMapping();
1225: else if (im.subfieldName.equals("key"))
1226: cm = mt.getKeyMapping();
1227: else if (im.subfieldName.equals("value"))
1228: cm = mt.getValueMapping();
1229: else
1230: throw new JDOUserException(
1231: "Invalid pseudo-field name "
1232: + im.subfieldName
1233: + " in macro "
1234: + im
1235: + ", must be \"owner\", \"key\", or \"value\"");
1236: } else
1237: throw new JDOUserException(
1238: "Field "
1239: + im.clazz.getName()
1240: + '.'
1241: + im.fieldName
1242: + " has no table of its own in which to look for "
1243: + im.subfieldName);
1244: }
1245: }
1246:
1247: im.value = cm.getColumn().getName().toString();
1248: }
1249:
1250: /*----------------------------- Inner classes ----------------------------*/
1251:
1252: /**
1253: * An abstract base class for StoreManager transactions that perform some
1254: * management function on the database.
1255: * <p>
1256: * Management transactions may be retried in the face of SQL exceptions to
1257: * work around failures caused by transient conditions, such as DB deadlocks.
1258: */
1259:
1260: private abstract class MgmtTransaction {
1261: public static final String MAX_RETRIES_PROPERTY = "com.triactive.jdo.store.maxRetries";
1262:
1263: protected final int isolationLevel;
1264: protected final int maxRetries;
1265:
1266: /**
1267: * Constructs a new management transaction having the given isolation
1268: * level.
1269: *
1270: * @param isolationLevel
1271: * One of the isolation level constants from java.sql.Connection.
1272: */
1273:
1274: public MgmtTransaction(int isolationLevel) {
1275: this .isolationLevel = isolationLevel;
1276:
1277: String s = System.getProperty(MAX_RETRIES_PROPERTY, "3");
1278: int mr;
1279:
1280: try {
1281: mr = Integer.parseInt(s);
1282: } catch (NumberFormatException e) {
1283: LOG.warn("Failed parsing " + MAX_RETRIES_PROPERTY
1284: + " property, value was " + s);
1285: mr = 3;
1286: }
1287:
1288: maxRetries = mr;
1289: }
1290:
1291: /**
1292: * Returns a description of the management transaction.
1293: * Subclasses should override this method so that transaction failures
1294: * are given an appropriate exception message.
1295: *
1296: * @return A description of the management transaction.
1297: */
1298:
1299: public abstract String toString();
1300:
1301: /**
1302: * Implements the body of the transaction.
1303: *
1304: * @param conn
1305: * A connection to the database. If the selected isolation level
1306: * is Connection.TRANSACTION_NONE the connection has auto-commit
1307: * set to true, otherwise auto-commit is false.
1308: *
1309: * @exception SQLException
1310: * If the transaction fails due to a database error that should
1311: * allow the entire transaction to be retried.
1312: */
1313:
1314: protected abstract void execute(Connection conn)
1315: throws SQLException;
1316:
1317: /**
1318: * Executes the transaction.
1319: * <p>
1320: * A database connection is acquired and the {@link #execute(Connection)}
1321: * method is invoked.
1322: * If the selected isolation level is not Connection.TRANSACTION_NONE,
1323: * then commit() or rollback() is called on the connection according to
1324: * whether the invocation succeeded or not.
1325: * If the invocation failed the sequence is repeated, up to a maximum of
1326: * <var>maxRetries</var> times, configurable by the system property
1327: * com.triactive.jdo.store.maxRetries.
1328: *
1329: * @exception JDODataStoreException
1330: * If a SQL exception occurred even after <var>maxRetries</var>
1331: * attempts.
1332: */
1333:
1334: public final void execute() {
1335: int attempts = 0;
1336:
1337: for (;;) {
1338: try {
1339: Connection conn = dba.getConnection(ds, userName,
1340: password, isolationLevel);
1341:
1342: try {
1343: boolean succeeded = false;
1344:
1345: try {
1346: execute(conn);
1347: succeeded = true;
1348: } finally {
1349: if (isolationLevel != Connection.TRANSACTION_NONE) {
1350: if (succeeded)
1351: conn.commit();
1352: else
1353: conn.rollback();
1354: }
1355: }
1356: } finally {
1357: dba.closeConnection(conn);
1358: }
1359:
1360: break;
1361: } catch (SQLException e) {
1362: SQLState state = dba.getSQLState(e);
1363:
1364: if ((state != null && !state.isWorthRetrying())
1365: || ++attempts >= maxRetries)
1366: throw new JDODataStoreException(
1367: "SQL exception: " + this , e);
1368: }
1369: }
1370: }
1371: }
1372:
1373: /**
1374: * A management transaction that adds a set of classes to the StoreManager,
1375: * making them usable for persistence.
1376: * <p>
1377: * This class embodies the work necessary to activate a persistent class and
1378: * ready it for storage management.
1379: * It is the primary mutator of a StoreManager.
1380: * <p>
1381: * Adding classes is an involved process that includes the creation and/or
1382: * validation in the database of tables, views, and table constraints, and
1383: * their corresponding Java objects maintained by the StoreManager.
1384: * Since it's a management transaction, the entire process is subject to
1385: * retry on SQL exceptions.
1386: * It is responsible for ensuring that the procedure either adds <i>all</i>
1387: * of the requested classes successfully, or adds none of them and preserves
1388: * the previous state of the StoreManager exactly as it was.
1389: */
1390:
1391: private class ClassAdder extends MgmtTransaction {
1392: private final Class[] classes;
1393: private Connection schemaConnection = null;
1394:
1395: /**
1396: * Constructs a new class adder transaction that will add the given
1397: * classes to the StoreManager.
1398: *
1399: * @param classes The class(es) to be added.
1400: */
1401:
1402: public ClassAdder(Class[] classes) {
1403: super (Connection.TRANSACTION_SERIALIZABLE);
1404:
1405: this .classes = classes;
1406: }
1407:
1408: public String toString() {
1409: return "Add classes to schema " + schemaName;
1410: }
1411:
1412: protected void execute(Connection conn) throws SQLException {
1413: synchronized (StoreManager.this ) {
1414: classAdder = this ;
1415: schemaConnection = conn;
1416:
1417: try {
1418: try {
1419: addClassTablesAndValidate(classes);
1420: } catch (NestedSQLException e) {
1421: throw e.getSQLException();
1422: }
1423: } finally {
1424: schemaConnection = null;
1425: classAdder = null;
1426: }
1427: }
1428: }
1429:
1430: /**
1431: * Called by StoreManager.addClasses() when it has been recursively
1432: * re-entered.
1433: * This just adds table objects for the requested classes and returns.
1434: *
1435: * @param classes The class(es) to be added.
1436: *
1437: * @exception NestedSQLException
1438: * If a SQL exception occurs it is wrapped within one of these.
1439: * The assumption is that the top-level ClassAdder.execute() will
1440: * pick it out and rethrow it as a SQLException so that the entire
1441: * ClassAdder transaction can be retried, if appropriate.
1442: */
1443:
1444: public void addClasses(Class[] classes) {
1445: if (schemaConnection == null)
1446: throw new IllegalStateException(
1447: "Add classes transaction is not active");
1448:
1449: try {
1450: addClassTables(classes);
1451: } catch (SQLException e) {
1452: throw new NestedSQLException(e);
1453: }
1454: }
1455:
1456: /**
1457: * Adds a new table object (ie ClassBaseTable or ClassView) for every class
1458: * in the given list that 1) requires an extent and 2) does not yet have an
1459: * extent (ie table) initialized in the store manager.
1460: *
1461: * <p>This doesn't initialize or validate the tables, it just adds the table
1462: * objects to the StoreManager's internal data structures.
1463: *
1464: * @param classes The class(es) whose tables are to be added.
1465: */
1466:
1467: private void addClassTables(Class[] classes)
1468: throws SQLException {
1469: Iterator i = getReferencedClasses(classes).iterator();
1470:
1471: while (i.hasNext()) {
1472: ClassMetaData cmd = (ClassMetaData) i.next();
1473:
1474: if (getTable(cmd) == null && cmd.requiresExtent()) {
1475: TableMetadata tmd = schemaTable.getTableMetadata(
1476: cmd, schemaConnection);
1477: ClassTable t;
1478:
1479: if (cmd.getViewDefinition(dba.getVendorID()) != null)
1480: t = new ClassView(tmd, cmd, StoreManager.this );
1481: else
1482: t = new ClassBaseTable(tmd, cmd,
1483: StoreManager.this );
1484:
1485: addTable(t);
1486: }
1487: }
1488:
1489: }
1490:
1491: /**
1492: * Returns a List of {@link ClassMetaData} objects representing the set of
1493: * classes consisting of the requested classes plus any and all other
1494: * classes they may reference, directly or indirectly.
1495: * The returned list is ordered by dependency.
1496: *
1497: * @param classes An array of persistence-capable classes.
1498: *
1499: * @return A List of <tt>ClassMetaData</tt> objects.
1500: *
1501: * @exception ClassNotPersistenceCapableException
1502: * If any of the given classes is not persistence-capable.
1503: */
1504:
1505: private List getReferencedClasses(Class[] classes) {
1506: List cmds = new ArrayList();
1507:
1508: for (int i = 0; i < classes.length; ++i) {
1509: ClassMetaData cmd = ClassMetaData.forClass(classes[i]);
1510:
1511: if (cmd == null)
1512: throw new ClassNotPersistenceCapableException(
1513: classes[i]);
1514:
1515: cmds
1516: .addAll(cmd.getReferencedClasses(dba
1517: .getVendorID()));
1518: }
1519:
1520: return cmds;
1521: }
1522:
1523: /**
1524: * Adds a new table object to the StoreManager's internal data structures.
1525: *
1526: * @param t The table to be added.
1527: */
1528:
1529: private void addTable(JDOTable t) {
1530: allTables.add(t);
1531:
1532: int tableID = t.getTableID();
1533:
1534: while (tablesByTableID.size() <= tableID)
1535: tablesByTableID.add(null);
1536:
1537: tablesByTableID.set(tableID, t);
1538: tablesByName.put(t.getName(), t);
1539: tablesByJavaID.put(t.getJavaName(), t);
1540: }
1541:
1542: /**
1543: * Adds a new table object (ie ClassBaseTable or ClassView) for every class
1544: * in the given list that 1) requires an extent and 2) does not yet have an
1545: * extent (ie table) initialized in the store manager.
1546: *
1547: * <p>After all of the table objects, including any other tables they might
1548: * reference, have been added, each table is initialized and validated in
1549: * the database.
1550: *
1551: * <p>If any error occurs along the way, any table(s) that were created are
1552: * dropped and the state of the StoreManager is rolled back to the point at
1553: * which this method was called.
1554: *
1555: * @param classes The class(es) whose tables are to be added.
1556: */
1557:
1558: private void addClassTablesAndValidate(Class[] classes)
1559: throws SQLException {
1560: ArrayList allTablesPrev = (ArrayList) allTables.clone();
1561: ArrayList tablesByTableIDPrev = (ArrayList) tablesByTableID
1562: .clone();
1563: HashMap tablesByNamePrev = (HashMap) tablesByName.clone();
1564: HashMap tablesByJavaIDPrev = (HashMap) tablesByJavaID
1565: .clone();
1566:
1567: ArrayList baseTablesCreated = new ArrayList();
1568: ArrayList baseTableConstraintsCreated = new ArrayList();
1569: ArrayList viewsCreated = new ArrayList();
1570: boolean completed = false;
1571:
1572: try {
1573: /* Add ClassTable's for the requested classes. */
1574: addClassTables(classes);
1575:
1576: /*
1577: * Repeatedly loop over the set of all table objects, initializing
1578: * any that need initialization. Each time a table object is
1579: * initialized, it may cause other associated table objects to be
1580: * added (via callbacks to addClasses()), so the loop must be
1581: * repeated until no more tables exist that need initialization.
1582: */
1583: ArrayList newBaseTables = new ArrayList();
1584: ArrayList newViews = new ArrayList();
1585: boolean someNeededInitialization;
1586:
1587: do {
1588: someNeededInitialization = false;
1589:
1590: Iterator i = ((ArrayList) allTables.clone())
1591: .iterator();
1592:
1593: while (i.hasNext()) {
1594: JDOTable t = (JDOTable) i.next();
1595:
1596: if (!t.isInitialized()) {
1597: t.initialize();
1598:
1599: if (t instanceof View)
1600: newViews.add(t);
1601: else
1602: newBaseTables.add(t);
1603:
1604: someNeededInitialization = true;
1605: }
1606: }
1607: } while (someNeededInitialization);
1608:
1609: /*
1610: * For each new BaseTable object, validate it against the actual
1611: * table in the database. If the table doesn't exist it is created
1612: * (subject to auto-create mode). Views are done later.
1613: */
1614: Iterator i = newBaseTables.iterator();
1615:
1616: while (i.hasNext()) {
1617: BaseTable t = (BaseTable) i.next();
1618:
1619: if (t.validate(tableValidationFlags,
1620: schemaConnection))
1621: baseTablesCreated.add(t);
1622:
1623: /* Discard any cached column info used to validate the table. */
1624: columnInfoByTableName.remove(t.getName());
1625: }
1626:
1627: /*
1628: * Iterate over the newly added table objects again, this time
1629: * validating their required constraints against the actual
1630: * constraints existing in the database. If the constraints don't
1631: * exist, they are created (subject to auto-create mode).
1632: *
1633: * Constraint processing is done as a separate step from table
1634: * validation because you can't create constraints that reference
1635: * other tables until those tables exist.
1636: */
1637: i = newBaseTables.iterator();
1638:
1639: while (i.hasNext()) {
1640: BaseTable t = (BaseTable) i.next();
1641:
1642: if (t
1643: .validateConstraints(
1644: constraintValidationFlags,
1645: schemaConnection))
1646: baseTableConstraintsCreated.add(t);
1647: }
1648:
1649: /*
1650: * For each new View object, validate it against the actual view in
1651: * the database. If the view doesn't exist it is created (subject
1652: * to auto-create mode).
1653: */
1654: i = newViews.iterator();
1655:
1656: while (i.hasNext()) {
1657: View v = (View) i.next();
1658:
1659: if (v.validate(tableValidationFlags,
1660: schemaConnection))
1661: viewsCreated.add(v);
1662:
1663: /* Discard any cached column info used to validate the view. */
1664: columnInfoByTableName.remove(v.getName());
1665: }
1666:
1667: completed = true;
1668: } finally {
1669: /*
1670: * If something went wrong, roll things back to the way they were
1671: * before we started. This may not restore the database 100% of the
1672: * time (if DDL statements are not transactional) but it will always
1673: * put the StoreManager's internal structures back the way they
1674: * were.
1675: */
1676: if (!completed) {
1677: allTables = allTablesPrev;
1678: tablesByTableID = tablesByTableIDPrev;
1679: tablesByName = tablesByNamePrev;
1680: tablesByJavaID = tablesByJavaIDPrev;
1681:
1682: /*
1683: * Tables, table constraints, and views get removed in the
1684: * reverse order from which they were created.
1685: */
1686: try {
1687: ListIterator li = viewsCreated
1688: .listIterator(viewsCreated.size());
1689:
1690: while (li.hasPrevious())
1691: ((View) li.previous())
1692: .drop(schemaConnection);
1693:
1694: li = baseTableConstraintsCreated
1695: .listIterator(baseTableConstraintsCreated
1696: .size());
1697:
1698: while (li.hasPrevious())
1699: ((BaseTable) li.previous())
1700: .dropConstraints(schemaConnection);
1701:
1702: li = baseTablesCreated
1703: .listIterator(baseTablesCreated.size());
1704:
1705: while (li.hasPrevious())
1706: ((BaseTable) li.previous())
1707: .drop(schemaConnection);
1708: } catch (Exception e) {
1709: LOG
1710: .warn("An error occurred while auto-creating schema elements. The following exception occurred while attempting to rollback the partially-completed schema changes: "
1711: + e.toString());
1712: }
1713: }
1714: }
1715: }
1716:
1717: /**
1718: * Called by Mapping objects in the midst of StoreManager.addClasses()
1719: * to request the creation of a set table.
1720: *
1721: * @param cbt The base table for the class containing the set.
1722: * @param fmd The field metadata describing the set field.
1723: *
1724: * @exception NestedSQLException
1725: * If a SQL exception occurs it is wrapped within one of these.
1726: * The assumption is that the top-level ClassAdder.execute() will
1727: * pick it out and rethrow it as a SQLException so that the entire
1728: * ClassAdder transaction can be retried, if appropriate.
1729: */
1730:
1731: public SetTable newSetTable(ClassBaseTable cbt,
1732: FieldMetaData fmd) {
1733: TableMetadata tmd;
1734:
1735: try {
1736: tmd = schemaTable.getTableMetadata(fmd,
1737: schemaConnection);
1738: } catch (SQLException e) {
1739: throw new NestedSQLException(e);
1740: }
1741:
1742: SetTable st = new SetTable(tmd, fmd, StoreManager.this );
1743:
1744: addTable(st);
1745:
1746: return st;
1747: }
1748:
1749: /**
1750: * Called by Mapping objects in the midst of StoreManager.addClasses()
1751: * to request the creation of a map table.
1752: *
1753: * @param cbt The base table for the class containing the map.
1754: * @param fmd The field metadata describing the map field.
1755: *
1756: * @exception NestedSQLException
1757: * If a SQL exception occurs it is wrapped within one of these.
1758: * The assumption is that the top-level ClassAdder.execute() will
1759: * pick it out and rethrow it as a SQLException so that the entire
1760: * ClassAdder transaction can be retried, if appropriate.
1761: */
1762:
1763: public MapTable newMapTable(ClassBaseTable cbt,
1764: FieldMetaData fmd) {
1765: TableMetadata tmd;
1766:
1767: try {
1768: tmd = schemaTable.getTableMetadata(fmd,
1769: schemaConnection);
1770: } catch (SQLException e) {
1771: throw new NestedSQLException(e);
1772: }
1773:
1774: MapTable mt = new MapTable(tmd, fmd, StoreManager.this );
1775:
1776: addTable(mt);
1777:
1778: return mt;
1779: }
1780: }
1781:
1782: /**
1783: * A runtime exception designed to tunnel a SQL exception from a deeply
1784: * nested recursive procedure up to a higher level that can handle it.
1785: */
1786:
1787: private static class NestedSQLException extends RuntimeException {
1788: private final SQLException e;
1789:
1790: public NestedSQLException(SQLException e) {
1791: super (
1792: "Inner invocation of recursive procedure threw a SQL exception: "
1793: + e.getMessage());
1794: this .e = e;
1795: }
1796:
1797: public SQLException getSQLException() {
1798: return e;
1799: }
1800: }
1801: }
|