/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.ejb.plugins.cmp.jdbc; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.List; import java.util.Iterator; import java.rmi.RemoteException; import javax.ejb.CreateException; import javax.ejb.EJBException; import javax.ejb.FinderException; import javax.ejb.RemoveException; import javax.transaction.Status; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.jboss.deployment.DeploymentException; import org.jboss.ejb.Container; import org.jboss.ejb.EjbModule; import org.jboss.ejb.EntityContainer; import org.jboss.ejb.EntityEnterpriseContext; import org.jboss.ejb.GenericEntityObjectFactory; import org.jboss.ejb.plugins.cmp.ejbql.Catalog; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCAbstractEntityBridge; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCApplicationMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCEntityMetaData; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCXmlFileLoader; import org.jboss.logging.Logger; import org.jboss.metadata.ApplicationMetaData; import org.jboss.tm.TransactionLocal; /** * JDBCStoreManager manages storage of persistence data into a table. * Other then loading the initial jbosscmp-jdbc.xml file this class * does very little. The interesting tasks are performed by the command * classes. * * Life-cycle: * Tied to the life-cycle of the entity container. * * Multiplicity: * One per cmp entity bean. This could be less if another implementaion of * EntityPersistenceStore is created and thoes beans use the implementation * * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a> * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a> * @see org.jboss.ejb.EntityPersistenceStore * @version $Revision: 81030 $ */ public final class JDBCStoreManager implements JDBCEntityPersistenceStore { /** The key used to store the tx data map. */ private static final Object TX_DATA_KEY = "TX_DATA_KEY"; /** The key to store the Catalog */ private static final String CATALOG = "CATALOG"; private static final String CREATED_MANAGERS = "CREATED_JDBCStoreManagers"; private static final String CMP_JDBC = "CMP-JDBC"; private EjbModule ejbModule; private EntityContainer container; private Logger log; private JDBCEntityMetaData metaData; private JDBCEntityBridge entityBridge; private JDBCTypeFactory typeFactory; private JDBCQueryManager queryManager; private JDBCCommandFactory commandFactory; private ReadAheadCache readAheadCache; // Manager life cycle commands private JDBCInitCommand initCommand; private JDBCStartCommand startCommand; private JDBCStopCommand stopCommand; private JDBCDestroyCommand destroyCommand; // Entity life cycle commands private JDBCCreateBeanClassInstanceCommand createBeanClassInstanceCommand; private JDBCInitEntityCommand initEntityCommand; private JDBCFindEntityCommand findEntityCommand; private JDBCFindEntitiesCommand findEntitiesCommand; private JDBCCreateCommand createEntityCommand; private JDBCPostCreateEntityCommand postCreateEntityCommand; private JDBCRemoveEntityCommand removeEntityCommand; private JDBCLoadEntityCommand loadEntityCommand; private JDBCIsModifiedCommand isModifiedCommand; private JDBCStoreEntityCommand storeEntityCommand; private JDBCActivateEntityCommand activateEntityCommand; private JDBCPassivateEntityCommand passivateEntityCommand; // commands private JDBCLoadRelationCommand loadRelationCommand; private JDBCDeleteRelationsCommand deleteRelationsCommand; private JDBCInsertRelationsCommand insertRelationsCommand; /** A Transaction manager so that we can link preloaded data to a transaction */ private TransactionManager tm; private TransactionLocal txDataMap; /** Set of EJBLocalObject instances to be cascade-deleted excluding those that should be batch-cascade-deleted. */ private TransactionLocal cascadeDeleteSet = new TransactionLocal() { protected Object initialValue() { return new CascadeDeleteRegistry(); } }; /** * Gets the container for this entity. * @return the container for this entity; null if container has not been set */ public EntityContainer getContainer() { return container; } /** * Sets the container for this entity. * @param container the container for this entity * @throws ClassCastException if the container is not an instance of * EntityContainer */ public void setContainer(Container container) { this.container = (EntityContainer)container; if(container != null) { ejbModule = container.getEjbModule(); log = Logger.getLogger( this.getClass().getName() + "." + container.getBeanMetaData().getEjbName()); } else { ejbModule = null; } } public JDBCAbstractEntityBridge getEntityBridge() { return entityBridge; } public JDBCTypeFactory getJDBCTypeFactory() { return typeFactory; } public JDBCEntityMetaData getMetaData() { return metaData; } public JDBCQueryManager getQueryManager() { return queryManager; } public JDBCCommandFactory getCommandFactory() { return commandFactory; } public ReadAheadCache getReadAheadCache() { return readAheadCache; } // // Genertic data containers // public Map getApplicationDataMap() { return ejbModule.getModuleDataMap(); } public Object getApplicationData(Object key) { return ejbModule.getModuleData(key); } public void putApplicationData(Object key, Object value) { ejbModule.putModuleData(key, value); } private Map getApplicationTxDataMap() { try { Transaction tx = tm.getTransaction(); if(tx == null) { return null; } // get the txDataMap from the txMap Map txMap = (Map)txDataMap.get(tx); // do we have an existing map if(txMap == null) { int status = tx.getStatus(); if(status == Status.STATUS_ACTIVE || status == Status.STATUS_PREPARING) { // create and add the new map txMap = new HashMap(); txDataMap.set(tx, txMap); } } return txMap; } catch(EJBException e) { throw e; } catch(Exception e) { throw new EJBException("Error getting application tx data map.", e); } } /** * Schedules instances for cascade-delete */ public void scheduleCascadeDelete(List pks) { CascadeDeleteRegistry registry = (CascadeDeleteRegistry)cascadeDeleteSet.get(); registry.scheduleAll(pks); } /** * Unschedules instance cascade delete. * @param pk instance primary key. * @return true if the instance was scheduled for cascade deleted. */ public boolean unscheduledCascadeDelete(Object pk) { CascadeDeleteRegistry registry = (CascadeDeleteRegistry)cascadeDeleteSet.get(); return registry.unschedule(pk); } public Object getApplicationTxData(Object key) { Map map = getApplicationTxDataMap(); if(map != null) { return map.get(key); } return null; } public void putApplicationTxData(Object key, Object value) { Map map = getApplicationTxDataMap(); if(map != null) { map.put(key, value); } } private Map getEntityTxDataMap() { Map entityTxDataMap = (Map)getApplicationTxData(this); if(entityTxDataMap == null) { entityTxDataMap = new HashMap(); putApplicationTxData(this, entityTxDataMap); } return entityTxDataMap; } public Object getEntityTxData(Object key) { return getEntityTxDataMap().get(key); } public void putEntityTxData(Object key, Object value) { getEntityTxDataMap().put(key, value); } public void removeEntityTxData(Object key) { getEntityTxDataMap().remove(key); } public Catalog getCatalog() { return (Catalog)getApplicationData(CATALOG); } private void initApplicationDataMap() { Map moduleData = ejbModule.getModuleDataMap(); synchronized(moduleData) { txDataMap = (TransactionLocal)moduleData.get(TX_DATA_KEY); if(txDataMap == null) { txDataMap = new TransactionLocal(); moduleData.put(TX_DATA_KEY, txDataMap); } } } /** * Does almost nothing because other services such * as JDBC data sources may not have been started. */ public void create() throws Exception { // Store a reference to this manager in an application level hashtable. // This way in the start method other managers will be able to know // the other managers. HashMap managersMap = (HashMap)getApplicationData(CREATED_MANAGERS); if(managersMap == null) { managersMap = new HashMap(); putApplicationData(CREATED_MANAGERS, managersMap); } managersMap.put(container.getBeanMetaData().getEjbName(), this); } /** * Bring the store to a fully initialized state */ public void start() throws Exception { // // // Start Phase 1: create bridge and commands but // don't access other entities initStoreManager(); // If all managers have been started (this is the last manager), // complete the other two phases of startup. Catalog catalog = getCatalog(); HashMap managersMap = (HashMap)getApplicationData(CREATED_MANAGERS); if(catalog.getEntityCount() == managersMap.size() && catalog.getEJBNames().equals(managersMap.keySet())) { // Make a copy of the managers (for safty) ArrayList managers = new ArrayList(managersMap.values()); // // // Start Phase 2: resolve relationships for(int i = 0; i < managers.size(); ++i) { JDBCStoreManager manager = (JDBCStoreManager)managers.get(i); manager.resolveRelationships(); } // // // Start Phase 3: create tables and compile queries for(int i = 0; i < managers.size(); ++i) { JDBCStoreManager manager = (JDBCStoreManager)managers.get(i); manager.startStoreManager(); } // add foreign key constraints for(int i = 0; i < managers.size(); ++i) { JDBCStoreManager manager = (JDBCStoreManager)managers.get(i); manager.startCommand.addForeignKeyConstraints(); } } } /** * Preforms as much initialization as possible without referencing * another entity. */ private void initStoreManager() throws Exception { if(log.isDebugEnabled()) log.debug("Initializing CMP plugin for " + container.getBeanMetaData().getEjbName()); // get the transaction manager tm = container.getTransactionManager(); // initializes the generic data containers initApplicationDataMap(); // load the metadata for this entity metaData = loadJDBCEntityMetaData(); // setup the type factory, which is used to map java types to sql types. typeFactory = new JDBCTypeFactory( metaData.getTypeMapping(), metaData.getJDBCApplication().getValueClasses(), metaData.getJDBCApplication().getUserTypeMappings() ); // create the bridge between java land and this engine (sql land) entityBridge = new JDBCEntityBridge(metaData, this); entityBridge.init(); // add the entity bridge to the catalog Catalog catalog = getCatalog(); if(catalog == null) { catalog = new Catalog(); putApplicationData(CATALOG, catalog); } catalog.addEntity(entityBridge); // create the read ahead cache readAheadCache = new ReadAheadCache(this); readAheadCache.create(); // Set up Commands commandFactory = new JDBCCommandFactory(this); // Execute the init command initCommand = commandFactory.createInitCommand(); initCommand.execute(); } private void resolveRelationships() throws Exception { entityBridge.resolveRelationships(); } /** * Brings the store manager into a completely running state. * This method will create the database table and compile the queries. */ private void startStoreManager() throws Exception { entityBridge.start(); // Store manager life cycle commands startCommand = commandFactory.createStartCommand(); stopCommand = commandFactory.createStopCommand(); destroyCommand = commandFactory.createDestroyCommand(); // Entity commands initEntityCommand = commandFactory.createInitEntityCommand(); createBeanClassInstanceCommand = commandFactory.createCreateBeanClassInstanceCommand(); findEntityCommand = commandFactory.createFindEntityCommand(); findEntitiesCommand = commandFactory.createFindEntitiesCommand(); createEntityCommand = commandFactory.createCreateEntityCommand(); postCreateEntityCommand = commandFactory.createPostCreateEntityCommand(); removeEntityCommand = commandFactory.createRemoveEntityCommand(); loadEntityCommand = commandFactory.createLoadEntityCommand(); isModifiedCommand = commandFactory.createIsModifiedCommand(); storeEntityCommand = commandFactory.createStoreEntityCommand(); activateEntityCommand = commandFactory.createActivateEntityCommand(); passivateEntityCommand = commandFactory.createPassivateEntityCommand(); // Relation commands loadRelationCommand = commandFactory.createLoadRelationCommand(); deleteRelationsCommand = commandFactory.createDeleteRelationsCommand(); insertRelationsCommand = commandFactory.createInsertRelationsCommand(); // Create the query manager queryManager = new JDBCQueryManager(this); // Execute the start command, creates the tables startCommand.execute(); // Start the query manager. At this point is creates all of the // query commands. The must occure in the start phase, as // queries can opperate on other entities in the application, and // all entities are gaurenteed to be createed until the start phase. queryManager.start(); readAheadCache.start(); } public void stop() { // On deploy errors, sometimes CMPStoreManager was never initialized! if(stopCommand != null) { Map managersMap = (HashMap)getApplicationData(CREATED_MANAGERS); while(!managersMap.isEmpty()) { int stoppedInIteration = 0; for(Iterator i = managersMap.values().iterator(); i.hasNext();) { JDBCStoreManager manager = (JDBCStoreManager)i.next(); if(manager.stopCommand == null || manager.stopCommand.execute()) { i.remove(); ++stoppedInIteration; } } if(stoppedInIteration == 0) { break; } } } readAheadCache.stop(); } public void destroy() { // On deploy errors, sometimes CMPStoreManager was never initialized! if(destroyCommand != null) { destroyCommand.execute(); } if(readAheadCache != null) { readAheadCache.destroy(); } readAheadCache = null; if(queryManager != null) { queryManager.clear(); } queryManager = null; //Remove proxy from proxy map so UnifiedClassloader may be released if(createBeanClassInstanceCommand != null) { createBeanClassInstanceCommand.destroy(); } // end of if () } // // EJB Life Cycle Commands // /** * Returns a new instance of a class which implemnts the bean class. * * @return the new instance */ public Object createBeanClassInstance() throws Exception { if(createBeanClassInstanceCommand == null) throw new IllegalStateException("createBeanClassInstanceCommand == null"); return createBeanClassInstanceCommand.execute(); } public void initEntity(EntityEnterpriseContext ctx) { initEntityCommand.execute(ctx); } public Object createEntity(Method createMethod, Object[] args, EntityEnterpriseContext ctx) throws CreateException { Object pk = createEntityCommand.execute(createMethod, args, ctx); if(pk == null) throw new CreateException("Primary key for created instance is null."); return pk; } public Object postCreateEntity(Method createMethod, Object[] args, EntityEnterpriseContext ctx) { return postCreateEntityCommand.execute(createMethod, args, ctx); } public Object findEntity(Method finderMethod, Object[] args, EntityEnterpriseContext ctx, GenericEntityObjectFactory factory) throws FinderException { return findEntityCommand.execute(finderMethod, args, ctx, factory); } public Collection findEntities(Method finderMethod, Object[] args, EntityEnterpriseContext ctx, GenericEntityObjectFactory factory) throws FinderException { return findEntitiesCommand.execute(finderMethod, args, ctx, factory); } public void activateEntity(EntityEnterpriseContext ctx) { activateEntityCommand.execute(ctx); } /** * Loads entity. * If entity not found NoSuchEntityException is thrown. * @param ctx - entity context. */ public void loadEntity(EntityEnterpriseContext ctx) { loadEntity(ctx, true); } public boolean loadEntity(EntityEnterpriseContext ctx, boolean failIfNotFound) { // is any on the data already in the entity valid if(!ctx.isValid()) { if(log.isTraceEnabled()) { log.trace("RESET PERSISTENCE CONTEXT: id=" + ctx.getId()); } entityBridge.resetPersistenceContext(ctx); } // mark the entity as created; if it was loading it was created JDBCEntityBridge.setCreated(ctx); return loadEntityCommand.execute(ctx, failIfNotFound); } public void loadField(JDBCCMPFieldBridge field, EntityEnterpriseContext ctx) { loadEntityCommand.execute(field, ctx); } public boolean isStoreRequired(EntityEnterpriseContext ctx) { return isModifiedCommand.execute(ctx); } public boolean isModified(EntityEnterpriseContext ctx) { return entityBridge.isModified(ctx); } public void storeEntity(EntityEnterpriseContext ctx) { storeEntityCommand.execute(ctx); synchronizeRelationData(); } private void synchronizeRelationData() { final JDBCCMRFieldBridge[] cmrFields = (JDBCCMRFieldBridge[]) entityBridge.getCMRFields(); for(int i = 0; i < cmrFields.length; ++i) { final JDBCCMRFieldBridge.RelationDataManager relationManager = cmrFields[i].getRelationDataManager(); if(relationManager.isDirty()) { final RelationData relationData = relationManager.getRelationData(); deleteRelations(relationData); insertRelations(relationData); relationData.addedRelations.clear(); relationData.removedRelations.clear(); relationData.notRelatedPairs.clear(); } } } public void passivateEntity(EntityEnterpriseContext ctx) { passivateEntityCommand.execute(ctx); } public void removeEntity(EntityEnterpriseContext ctx) throws RemoveException, RemoteException { removeEntityCommand.execute(ctx); } // // Relationship Commands // public Collection loadRelation(JDBCCMRFieldBridge cmrField, Object pk) { return loadRelationCommand.execute(cmrField, pk); } private void deleteRelations(RelationData relationData) { deleteRelationsCommand.execute(relationData); } private void insertRelations(RelationData relationData) { insertRelationsCommand.execute(relationData); } private JDBCEntityMetaData loadJDBCEntityMetaData() throws DeploymentException { ApplicationMetaData amd = container.getBeanMetaData().getApplicationMetaData(); // Get JDBC MetaData JDBCApplicationMetaData jamd = (JDBCApplicationMetaData)amd.getPluginData(CMP_JDBC); if(jamd == null) { // we are the first cmp entity to need jbosscmp-jdbc. // Load jbosscmp-jdbc.xml for the whole application JDBCXmlFileLoader jfl = new JDBCXmlFileLoader(container, log); jamd = jfl.load(); amd.addPluginData(CMP_JDBC, jamd); } // Get JDBC Bean MetaData String ejbName = container.getBeanMetaData().getEjbName(); JDBCEntityMetaData metadata = jamd.getBeanByEjbName(ejbName); if(metadata == null) { throw new DeploymentException("No metadata found for bean " + ejbName); } return metadata; } // Inner private final class CascadeDeleteRegistry { private Set scheduled; public void scheduleAll(List pks) { if(scheduled == null) { scheduled = new HashSet(); } scheduled.addAll(pks); } public boolean unschedule(Object pk) { return scheduled.remove(pk); } } }