/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.persistence.ejb.entitybean.container; import java.lang.reflect.Method; import java.rmi.RemoteException; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import javax.ejb.CreateException; import javax.ejb.EJBException; import javax.ejb.EJBLocalObject; import javax.ejb.EJBObject; import javax.ejb.EntityBean; import javax.ejb.FinderException; import javax.ejb.NoSuchEntityException; import javax.ejb.NoSuchObjectLocalException; import javax.ejb.RemoveException; import com.sun.ejb.ComponentContext; import com.sun.ejb.EjbInvocation; import com.sun.ejb.InvocationInfo; import com.sun.ejb.containers.EJBContextImpl; import com.sun.ejb.containers.EJBHomeInvocationHandler; import com.sun.ejb.containers.EJBLocalHomeInvocationHandler; import com.sun.ejb.containers.EJBLocalRemoteObject; import com.sun.enterprise.security.SecurityManager; import org.glassfish.persistence.ejb.entitybean.container.cache.EJBObjectCache; import org.glassfish.persistence.ejb.entitybean.container.cache.FIFOEJBObjectCache; import org.glassfish.persistence.ejb.entitybean.container.cache.UnboundedEJBObjectCache; import com.sun.ejb.spi.container.BeanStateSynchronization; import org.glassfish.ejb.deployment.descriptor.EjbDescriptor; import org.glassfish.ejb.deployment.descriptor.EjbEntityDescriptor; import org.glassfish.persistence.ejb.entitybean.container.distributed.DistributedEJBServiceFactory; import org.glassfish.persistence.ejb.entitybean.container.distributed.DistributedReadOnlyBeanService; import org.glassfish.persistence.ejb.entitybean.container.distributed.ReadOnlyBeanRefreshEventHandler; import static com.sun.ejb.containers.EJBContextImpl.BeanState; /** * The Container that manages instances of ReadOnly Beans. This container * blocks all calls to ejbStore() and selectively performs ejbLoad() * * @author Mahesh Kannan * @author Pramod Gopinath */ public class ReadOnlyBeanContainer extends EntityContainer implements ReadOnlyBeanRefreshEventHandler { private long refreshPeriodInMillis = 0; // Sequence number incremented each time a bean-level refresh is requested. // PK-level data structure has a corresponding sequence number that is used // to determine when it needs updating due to bean-level refresh. private int beanLevelSequenceNum = 1; // Last time a bean-level timeout refresh event occurred. private long beanLevelLastRefreshRequestedAt = 0; private volatile long currentTimeInMillis = System.currentTimeMillis(); // timer task for refreshing or null if no refresh. private TimerTask refreshTask = null; private EJBObjectCache robCache; private DistributedReadOnlyBeanService distributedReadOnlyBeanService; private volatile Map<FinderResultsKey, FinderResultsValue> finderResultsCache = new ConcurrentHashMap<FinderResultsKey, FinderResultsValue>(); private static final int FINDER_LOCK_SIZE = 8 * 1024; private Object[] finderLocks = new Object[FINDER_LOCK_SIZE]; //Don't make this as a static. In future, we may want to // support bean level flag for this private boolean RELATIVE_TIME_CHECK_MODE = false; protected ReadOnlyBeanContainer(EjbDescriptor desc, ClassLoader loader, SecurityManager sm) throws Exception { //super(ContainerType.READ_ONLY, desc, loader); super(ContainerType.ENTITY, desc, loader, sm); EjbEntityDescriptor ed = (EjbEntityDescriptor)desc; refreshPeriodInMillis = ed.getIASEjbExtraDescriptors().getRefreshPeriodInSeconds() * 1000L; if( refreshPeriodInMillis > 0 ) { long timerFrequency = 1; String refreshRateStr = System.getProperty("com.sun.ejb.containers.readonly.timer.frequency", "1"); try { timerFrequency = Integer.parseInt(refreshRateStr); if (timerFrequency < 0) { timerFrequency = 1; } } catch (Exception ex) { _logger.log(Level.FINE, "Invalid timer frequency " + refreshRateStr); } try { RELATIVE_TIME_CHECK_MODE = Boolean.valueOf(System.getProperty( "com.sun.ejb.containers.readonly.relative.refresh.mode")); _logger.log(Level.FINE, "RELATIVE_TIME_CHECK_MODE: " + RELATIVE_TIME_CHECK_MODE); } catch (Exception ex) { _logger.log(Level.FINE, "(Ignorable) Exception while initializing RELATIVE_TIME_CHECK_MODE", ex); } Timer timer = ejbContainerUtilImpl.getTimer(); if (RELATIVE_TIME_CHECK_MODE) { refreshTask = new CurrentTimeRefreshTask (); timer.scheduleAtFixedRate(refreshTask, timerFrequency*1000L, timerFrequency*1000L); } else { refreshTask = new RefreshTask(); timer.scheduleAtFixedRate(refreshTask, refreshPeriodInMillis, refreshPeriodInMillis); } } else { refreshPeriodInMillis = 0; } for (int i=0; i<FINDER_LOCK_SIZE; i++) { finderLocks[i] = new Object(); } // Create read-only bean cache long idleTimeoutInMillis = (cacheProp.cacheIdleTimeoutInSeconds <= 0) ? -1 : (cacheProp.cacheIdleTimeoutInSeconds * 1000L); if( (cacheProp.maxCacheSize <= 0) && (idleTimeoutInMillis <= 0) ) { robCache = new UnboundedEJBObjectCache(ejbDescriptor.getName()); robCache.init(DEFAULT_CACHE_SIZE, cacheProp.numberOfVictimsToSelect, 0L, 1.0F, null); } else { int cacheSize = (cacheProp.maxCacheSize <= 0) ? DEFAULT_CACHE_SIZE : cacheProp.maxCacheSize; robCache = new FIFOEJBObjectCache(ejbDescriptor.getName()); robCache.init(cacheSize, cacheProp.numberOfVictimsToSelect, idleTimeoutInMillis, 1.0F, null); // .setEJBObjectCacheListener( // new EJBObjectCacheVictimHandler()); } this.distributedReadOnlyBeanService = DistributedEJBServiceFactory.getDistributedEJBService() .getDistributedReadOnlyBeanService(); this.distributedReadOnlyBeanService.addReadOnlyBeanRefreshEventHandler( getContainerId(), getClassLoader(), this); } private void updateBeanLevelRefresh() { beanLevelSequenceNum++; beanLevelLastRefreshRequestedAt = this.currentTimeInMillis; if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, "updating bean-level refresh for " + " read-only bean " + ejbDescriptor.getName() + " at " + new Date(beanLevelLastRefreshRequestedAt) + " beanLevelSequenceNum = " + beanLevelSequenceNum); } // Clear out bean-level finder results cache. if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, "Clearing " + finderResultsCache.size() + " items from " + "finder results cache"); } finderResultsCache = new ConcurrentHashMap<FinderResultsKey, FinderResultsValue>(); } protected void callEJBStore(EntityBean ejb, EntityContextImpl context) { // this method in the ReadOnlyBean case should be a no-op // and should not throw any exception. } protected ComponentContext _getContext(EjbInvocation inv) { ComponentContext ctx = super._getContext(inv); InvocationInfo info = inv.invocationInfo; // info cannot be null if (info.isTxRequiredLocalCMPField) { if (! inv.foundInTxCache) { EntityContextImpl entityCtx = (EntityContextImpl) ctx; super.afterBegin(entityCtx); inv.foundInTxCache = true; } } else { //TODO: We can still optimize NonTx access to CMP getters/setters } return ctx; } protected void callEJBLoad(EntityBean ejb, EntityContextImpl entityCtx, boolean activeTx) throws Exception { // ReadOnlyContextImpl should always be used in conjunction with ReadOnlyBeanContainer assert entityCtx instanceof ReadOnlyContextImpl; ReadOnlyContextImpl context = (ReadOnlyContextImpl) entityCtx; ReadOnlyBeanInfo robInfo = context.getReadOnlyBeanInfo(); // Grab the pk-specific lock before doing the refresh comparisons. // In the common-case, the lock will only be held for a very short // amount of time. In the case where a pk-level refresh is needed, // we want to ensure that no concurrent refreshes for the same // pk can occur. int pkLevelSequenceNum = 0; long pkLastRefreshedAt = 0; synchronized(robInfo) { int currentBeanLevelSequenceNum = beanLevelSequenceNum; if( robInfo.beanLevelSequenceNum != currentBeanLevelSequenceNum) { if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, "REFRESH DUE TO BEAN-LEVEL UPDATE:" + " Bean-level sequence num = " + beanLevelSequenceNum + robInfo + " current time is " + new Date()); } robInfo.refreshNeeded = true; } else if (RELATIVE_TIME_CHECK_MODE && (refreshPeriodInMillis > 0)) { // 0 implies no time based refresh if ((currentTimeInMillis - robInfo.lastRefreshedAt) > refreshPeriodInMillis) { if (_logger.isLoggable(Level.FINE)) { _logger.log(Level.FINE, "REFRESH DUE TO STALE PK:" + " robInfo.lastRefreshedAt: " + robInfo.lastRefreshedAt + "; current (approx) time is " + currentTimeInMillis); } robInfo.refreshNeeded = true; } } // Refresh could be true EITHER because time-based refresh // occurred or programmatic refresh of this PK. if (robInfo.refreshNeeded) { if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, " PK-LEVEL REFRESH : " + robInfo + " current time is " + new Date()); } try { if( isContainerManagedPers ) { BeanStateSynchronization beanStateSynch = (BeanStateSynchronization) ejb; beanStateSynch.ejb__refresh(entityCtx.getPrimaryKey()); if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, " PK-LEVEL REFRESH DONE :" + robInfo + " current time is " + new Date()); } } else { if( ejb instanceof BeanStateSynchronization ) { // For debugging purposes, call into ejb__refresh // if it's present on a BMP bean class BeanStateSynchronization beanStateSynch = (BeanStateSynchronization) ejb; beanStateSynch.ejb__refresh (entityCtx.getPrimaryKey()); } } } finally { // Always set refreshNeeded to false robInfo.refreshNeeded = false; } // Rob info only updated if no errors so far. updateAfterRefresh(robInfo); } pkLevelSequenceNum = robInfo.pkLevelSequenceNum; pkLastRefreshedAt = robInfo.lastRefreshedAt; } // releases lock for pk's read-only bean info if ((entityCtx.isNewlyActivated()) || (context.getPKLevelSequenceNum() != pkLevelSequenceNum)) { // Now do instance-level refresh check to see if // ejbLoad is warranted. callLoad(ejb, context, pkLevelSequenceNum, pkLastRefreshedAt, currentTimeInMillis); } } private void callLoad(EntityBean ejb, ReadOnlyContextImpl context, int pkLevelSequenceNum, long pkLastRefreshedAt, long currentTime) throws Exception { if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, "Calling ejbLoad for read-only bean " + ejbDescriptor.getName() + " primary key " + context.getPrimaryKey() + " at " + new Date(currentTime)); } try { context.setInEjbLoad(true); ejb.ejbLoad(); if( pkLevelSequenceNum > 0 ) { // Synch up pk-level sequence num after successful load context.setPKLevelSequenceNum(pkLevelSequenceNum); } // Set last refresh time after successful load context.setLastRefreshedAt(pkLastRefreshedAt); } finally { context.setInEjbLoad(false); } } protected void callEJBRemove(EntityBean ejb, EntityContextImpl context) throws Exception { // This will only be called for BMP read-only beans since AS 7 // allowed the client to make this call. Calls to remove // CMP read-only beans result in a runtime exception. Object pk = context.getPrimaryKey(); robCache.removeAll(pk); } protected void doConcreteContainerShutdown(boolean appBeingUndeployed) { this.distributedReadOnlyBeanService.removeReadOnlyBeanRefreshEventHandler( getContainerId()); if( refreshTask != null ) { refreshTask.cancel(); } robCache.clear(); super.doConcreteContainerShutdown(appBeingUndeployed); } // Called from BaseContainer just before invoking a business method // whose tx attribute is TX_NEVER / TX_NOT_SUPPORTED / TX_SUPPORTS // without a client tx. protected void preInvokeNoTx(EjbInvocation inv) { EntityContextImpl context = (EntityContextImpl)inv.context; if ( context.isInState(BeanState.DESTROYED) ) return; if ( !inv.invocationInfo.isCreateHomeFinder ) { // follow EJB2.0 section 12.1.6.1 EntityBean e = (EntityBean)context.getEJB(); try { callEJBLoad(e, context, false); } catch ( NoSuchEntityException ex ) { _logger.log(Level.FINE, "Exception in preInvokeNoTx()", ex); // Error during ejbLoad, so discard bean: EJB2.0 18.3.3 forceDestroyBean(context); throw new NoSuchObjectLocalException( "NoSuchEntityException thrown by ejbLoad, " + "EJB instance discarded"); } catch ( Exception ex ) { // Error during ejbLoad, so discard bean: EJB2.0 18.3.3 forceDestroyBean(context); throw new EJBException(ex); } context.setNewlyActivated(false); } } protected void afterNewlyActivated(EntityContextImpl context) { // In the case of ReadOnlyBean store the Context into the list ReadOnlyBeanInfo robInfo = addToCache(context.getPrimaryKey(), true); // Set the read-only bean info on the context so we can access it // without doing a cache lookup. // ReadOnlyContextImpl should always be used in conjunction with ReadOnlyBeanContainer assert context instanceof ReadOnlyContextImpl; ReadOnlyContextImpl readOnlyContext = (ReadOnlyContextImpl) context; readOnlyContext.setReadOnlyBeanInfo(robInfo); } protected void addPooledEJB(EntityContextImpl ctx) { try { // ReadOnlyContextImpl should always be used in conjunction with ReadOnlyBeanContainer assert ctx instanceof ReadOnlyContextImpl; ReadOnlyContextImpl readOnlyCtx = (ReadOnlyContextImpl)ctx; if( readOnlyCtx.getReadOnlyBeanInfo() != null ) { readOnlyCtx.setReadOnlyBeanInfo(null); robCache.remove(ctx.getPrimaryKey(), true); } } catch (Exception ex) { _logger.log(Level.SEVERE, "entitybean.container.addPooledEJB", ex); EJBException ejbEx = new EJBException(); ejbEx.initCause(ex); throw ejbEx; } finally { super.addPooledEJB(ctx); } } protected void forceDestroyBean(EJBContextImpl context) { try { ReadOnlyContextImpl readOnlyCtx = (ReadOnlyContextImpl) context; if( readOnlyCtx.getReadOnlyBeanInfo() != null ) { readOnlyCtx.setReadOnlyBeanInfo(null); robCache.remove(readOnlyCtx.getPrimaryKey(), true); } } catch (Exception ex) { _logger.log(Level.SEVERE, "entitybean.container.forceDestroyBean", ex); EJBException ejbEx = new EJBException(); ejbEx.initCause(ex); throw ejbEx; } finally { super.forceDestroyBean(context); } } public void preInvoke(EjbInvocation inv) { // Overriding preInvoke is the best way to interpose on the // create early enough to throw an exception or eat the // request before too much setup work is done by the container. // It's better to keep this logic in the Read-Only Bean container // than to put it in the InvocationHandlers. Note that // interposition for the remove operation is handled below // by overriding the removeBean method. if( (inv.invocationInfo != null) && inv.invocationInfo.startsWithCreate ) { String msg = "Error for ejb " + ejbDescriptor.getName() + ". create is not allowed for read-only entity beans"; if( isContainerManagedPers ) { // EJB team decided that throwing a runtime exception was more // appropriate in this case since creation is not a // supported operation for read-only beans. If the application // is coded this way, it's best to throw a system exception // to signal that the application is broken. NOTE that this // only applies to the CMP 1.x and 2.x read-only bean // functionality added starting with AS 8.1. throw new EJBException(msg); } else { // Preserve AS 7 BMP ROB create behavior CreateException ce = new CreateException(msg); throw new PreInvokeException(ce); } } else { super.preInvoke(inv); } } protected Object invokeTargetBeanMethod(Method beanClassMethod, EjbInvocation inv, Object target, Object[] params, com.sun.enterprise.security.SecurityManager mgr) throws Throwable { Object returnValue = null; if( inv.invocationInfo.startsWithFind ) { FinderResultsKey key = new FinderResultsKey(inv.method, params); FinderResultsValue value = finderResultsCache.get(key); if (value != null) { if (RELATIVE_TIME_CHECK_MODE && (refreshPeriodInMillis > 0)) { long timeLeft = currentTimeInMillis - value.lastRefreshedAt; if (timeLeft >= refreshPeriodInMillis) { returnValue = value.value; } } else { //Use even if !RELATIVE_MODE or if refreshTime == 0 returnValue = value.value; } } if (returnValue == null) { int hashCode = key.getExtendedHC(); if (hashCode < 0) { hashCode = -hashCode; } int index = hashCode & (FINDER_LOCK_SIZE - 1); synchronized (finderLocks[index]) { value = finderResultsCache.get(key); if (value == null) { returnValue = super.invokeTargetBeanMethod( beanClassMethod, inv, target, params, mgr); finderResultsCache.put(key, new FinderResultsValue(returnValue, currentTimeInMillis)); } else { returnValue = value.value; } } } } else { returnValue = super.invokeTargetBeanMethod(beanClassMethod, inv, target, params, mgr); } return returnValue; } protected void removeBean(EJBLocalRemoteObject ejbo, Method removeMethod, boolean local) throws RemoveException, EJBException, RemoteException { String msg = "Error for ejb " + ejbDescriptor.getName() + ". remove is not allowed for read-only entity beans"; if( isContainerManagedPers ) { // EJB team decided that throwing a runtime exception was more // appropriate in this case since removal is not a // supported operation for read-only beans. If the application // is coded this way, it's best to throw a system exception // to signal that the application is broken. NOTE that this // only applies to the CMP 1.x and 2.x read-only bean // functionality added starting with AS 8.1. // There's no post-invoke logic to convert local exceptions // to remote, so take care of that here. if (local) { throw new EJBException(msg); } else { throw new RemoteException(msg); } } else { // Preserve AS 7 BMP ROB removal behavior. // Calls to ejbRemove on BMP read-only beans in AS 7 // were silently "eaten" by the ejb container. The // client didn't receive any exception, but ejbRemove // was not called on the container. } } protected void initializeHome() throws Exception { super.initializeHome(); if (isRemote) { ((ReadOnlyEJBHomeImpl) this.ejbHomeImpl). setReadOnlyBeanContainer(this); } if (isLocal) { ReadOnlyEJBLocalHomeImpl readOnlyLocalHomeImpl = (ReadOnlyEJBLocalHomeImpl) ejbLocalHomeImpl; readOnlyLocalHomeImpl.setReadOnlyBeanContainer(this); } } @Override protected EJBHomeInvocationHandler getEJBHomeInvocationHandler(Class homeIntfClass) throws Exception { return new ReadOnlyEJBHomeImpl(ejbDescriptor, homeIntfClass); } @Override protected EJBLocalHomeInvocationHandler getEJBLocalHomeInvocationHandler(Class homeIntfClass) throws Exception { return new ReadOnlyEJBLocalHomeImpl(ejbDescriptor, homeIntfClass); } public void setRefreshFlag(Object primaryKey) { try { handleRefreshRequest(primaryKey); } finally { distributedReadOnlyBeanService.notifyRefresh( getContainerId(), primaryKey); } } public void handleRefreshRequest(Object primaryKey) { // Lookup the read-only bean info for this pk. // If there is no entry for this pk, do nothing. // If there is a cache hit we *don't* want to increment the // ref count. ReadOnlyBeanInfo robInfo = (ReadOnlyBeanInfo) robCache.get(primaryKey, false); if( robInfo != null ) { synchronized(robInfo) { robInfo.refreshNeeded = true; robInfo.lastRefreshRequestedAt = this.currentTimeInMillis; if( _logger.isLoggable(Level.FINE) ) { _logger.log(Level.FINE, "Updating refresh time for read-only bean " + ejbDescriptor.getName() + " primary key " + primaryKey + " at " + new Date(robInfo.lastRefreshRequestedAt) + " pkLevelSequenceNum = " + robInfo.pkLevelSequenceNum); } } } else { _logger.log(Level.FINE, "Refresh event for unknown read-only bean PK = " + primaryKey + " at " + new Date()); } } /** * invoked when application calls refreshAll() */ void refreshAll() { try { handleRefreshAllRequest(); } finally { distributedReadOnlyBeanService.notifyRefreshAll(getContainerId()); } } public void handleRefreshAllRequest() { _logger.log(Level.FINE, "Received refreshAll request..."); updateBeanLevelRefresh(); } protected EntityContextImpl createEntityContextInstance(EntityBean ejb, EntityContainer entityContainer) { return new ReadOnlyContextImpl(ejb, entityContainer); } private ReadOnlyBeanInfo addToCache(Object primaryKey, boolean incrementRefCount) { // Optimize for the cache where the cache item already // exists and we have a 2nd, 3rd, 4th, etc. context for // the same primary key. If the item exists, the ref count // will be incremented. ReadOnlyBeanInfo robInfo = (ReadOnlyBeanInfo) robCache.get(primaryKey, incrementRefCount); if( robInfo == null ) { // If the item doesn't exist, create a new one. The cache // ensures that the ref count is correct in the face of concurrent // puts. ReadOnlyBeanInfo newRobInfo = new ReadOnlyBeanInfo(); newRobInfo.primaryKey = primaryKey; // Initialize bean level sequence num so that the first time an // instance of this PK goes through callEJBLoad, it will force // a refresh. newRobInfo.beanLevelSequenceNum = -1; newRobInfo.refreshNeeded = true; newRobInfo.pkLevelSequenceNum = 1; newRobInfo.lastRefreshRequestedAt = 0; newRobInfo.lastRefreshedAt = 0; // Cache ejbObject/ejbLocalObject within ROB info. // This value is used by // findByPrimaryKey to avoid a DB access. Caching here // ensures that there will be one DB access for the PK // regardless of the order in which findByPrimaryKey is called // with respect to the business method call. This also covers // the case where a business method is invoked through the // local view and findByPrimaryKey is invoked through the // Remote view (or vice versa). if( ejbDescriptor.isLocalInterfacesSupported() ) { newRobInfo.cachedEjbLocalObject = getEJBLocalObjectForPrimaryKey(primaryKey); } if( ejbDescriptor.isRemoteInterfacesSupported() ) { newRobInfo.cachedEjbObject = getEJBObjectStub(primaryKey, null); } ReadOnlyBeanInfo otherRobInfo = (ReadOnlyBeanInfo) robCache.put(primaryKey, newRobInfo, incrementRefCount); // If someone else inserted robInfo for this pk before *our* put(), // use that as the pk's robInfo. Otherwise, the new robInfo we // created is the "truth" for this pk. robInfo = (otherRobInfo == null) ? newRobInfo : otherRobInfo; } return robInfo; } //Called from InvocationHandler for findByPrimaryKey //The super class (EntityContainer) also defines this method whcih is where // the real work (of finding it from the database) is done. protected Object invokeFindByPrimaryKey(Method method, EjbInvocation inv, Object[] args) throws Throwable { Object returnValue = null; ReadOnlyBeanInfo robInfo = addToCache(args[0], false); synchronized (robInfo) { returnValue = inv.isLocal ? robInfo.cachedEjbLocalObject : robInfo.cachedEjbObject; if ( robInfo.refreshNeeded ) { _logger.log(Level.FINE, "ReadOnlyBeanContainer calling ejb.ejbFindByPK... for pk=" + args[0]); returnValue = super.invokeFindByPrimaryKey(method, inv, args); robInfo.refreshNeeded = false; //set the seq numbers so that the subsequent business method calls // (if within expiration time) do not have to call ejb__refresh!! updateAfterRefresh(robInfo); } } return returnValue; } public Object postFind(EjbInvocation inv, Object primaryKeys, Object[] findParams) throws FinderException { // Always call parent to convert pks to ejbobjects/ejblocalobjects. Object returnValue = super.postFind(inv, primaryKeys, findParams); // Only proceed if this is not a findByPK method. FindByPK // processing is special since it's possible to actually // skip the db access for the query itself. The caching requirements // to actually skip nonFindByPK queries are extremely complex, but // the next best thing to skipping the query is to populate the // RobInfo cache with an entry for each pk in the result set. If // a PK is part of the result set for a nonFindByPK query before // it is accessed through some other means, no new refresh will be // required. This will have the largest benefits for large result // sets since it's possible for a query to return N beans from one // db access, which would otherwise require N db accesses if the // refresh were done upon business method invocation or findByPK. // If a PK has been accessed before appearing in the result set of // a nonFindByPK finder, there is no performance gain. if( !inv.method.getName().equals("findByPrimaryKey") ) { if ( primaryKeys instanceof Enumeration ) { Enumeration e = (Enumeration) primaryKeys; while ( e.hasMoreElements() ) { Object primaryKey = e.nextElement(); if( primaryKey != null ) { updateRobInfoAfterFinder(primaryKey); } } } else if ( primaryKeys instanceof Collection ) { Collection c = (Collection)primaryKeys; Iterator it = c.iterator(); while ( it.hasNext() ) { Object primaryKey = it.next(); if( primaryKey != null ) { updateRobInfoAfterFinder(primaryKey); } } } else { if( primaryKeys != null ) { updateRobInfoAfterFinder(primaryKeys); } } } return returnValue; } private void updateRobInfoAfterFinder(Object primaryKey) { addToCache(primaryKey, false); /* ReadOnlyBeanInfo robInfo = addToCache(primaryKey, false); synchronized (robInfo) { if( robInfo.refreshNeeded ) { robInfo.refreshNeeded = false; updateAfterRefresh(robInfo); } } */ } //Called after a sucessful ejb_refresh and //it is assumed that the caller has a lock on the robInfo private void updateAfterRefresh(ReadOnlyBeanInfo robInfo) { robInfo.beanLevelSequenceNum = beanLevelSequenceNum; robInfo.pkLevelSequenceNum++; robInfo.lastRefreshedAt = this.currentTimeInMillis; } private final class RefreshTask extends TimerTask { public void run() { updateBeanLevelRefresh(); } } private final class CurrentTimeRefreshTask extends TimerTask { public void run() { currentTimeInMillis = System.currentTimeMillis(); } } private static final class FinderResultsValue { long lastRefreshedAt; Object value; public FinderResultsValue(Object v, long time) { value = v; this.lastRefreshedAt = time; } } private static final class FinderResultsKey { private static final Object[] EMPTY_PARAMS = new Object[0]; private Method finderMethod; private Object[] params; private int hc; private int extendedHC; public FinderResultsKey(Method method, Object[] params) { finderMethod = method; this.hc = finderMethod.hashCode(); this.extendedHC = this.hc; this.params = (params == null) ? EMPTY_PARAMS : params; for (Object param : this.params) { extendedHC += param.hashCode(); } } public int hashCode() { return hc; } public int getExtendedHC() { return this.extendedHC; } public boolean equals(Object o) { boolean equal = false; if( o instanceof FinderResultsKey ) { FinderResultsKey other = (FinderResultsKey) o; if ((params.length == other.params.length) && (finderMethod.equals(other.finderMethod))) { equal = true; for(int i = 0; i < params.length; i++) { Object nextParam = params[i]; Object nextParamOther = other.params[i]; if( nextParam instanceof EJBLocalObject ) { equal = compareEJBLocalObject (((EJBLocalObject)nextParam), nextParamOther); } else if ( nextParam instanceof EJBObject ) { equal = compareEJBObject (((EJBObject)nextParam), nextParamOther); } else { equal = nextParam.equals(nextParamOther); } if( !equal ) { break; } } } } return equal; } private boolean compareEJBLocalObject(EJBLocalObject localObj1, Object other) { boolean equal = false; if( other instanceof EJBLocalObject ) { equal = localObj1.isIdentical((EJBLocalObject) other); } return equal; } private boolean compareEJBObject(EJBObject ejbObj1, Object other) { boolean equal = false; if( other instanceof EJBObject ) { // @@@ Might want to optimize to avoid EJBObject invocation // overhead. try { equal = ejbObj1.isIdentical((EJBObject) other); } catch(RemoteException re) { // ignore equal = false; } } return equal; } } }