/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.orchestra.conversation.spring;
import org.aopalliance.aop.Advice;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.orchestra.conversation.Conversation;
import org.apache.myfaces.orchestra.conversation.ConversationAware;
import org.apache.myfaces.orchestra.conversation.ConversationBindingEvent;
import org.apache.myfaces.orchestra.conversation.ConversationBindingListener;
import org.apache.myfaces.orchestra.conversation.ConversationContext;
import org.apache.myfaces.orchestra.conversation.ConversationFactory;
import org.apache.myfaces.orchestra.conversation.ConversationManager;
import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.HashMap;
import java.util.Map;
/**
* Abstract basis class for all the Orchestra scopes.
* <p>
* A scope object has two quite different roles:
* <ol>
* <li>It handles the lookup of beans in a scope, and creates them if needed</li>
* <li>It handles the creation of Conversation objects, using the spring properties
* configured on the scope object.</li>
* </ol>
* <p>
* This base class handles item 1 above, and leaves item 2 to a subclass. The
* declaration of interface ConversationFactory needs to be on this class, however,
* as the createBean method needs to invoke it.
*/
public abstract class AbstractSpringOrchestraScope implements ConversationFactory,
Scope, BeanFactoryAware, ApplicationContextAware
{
private final Log log = LogFactory.getLog(AbstractSpringOrchestraScope.class);
private static final String POST_PROCESSOR_BEAN_NAME =
AbstractSpringOrchestraScope.class.getName() + "_BeanPostProcessor";
private ConfigurableApplicationContext applicationContext;
private Advice[] advices;
private boolean autoProxy = true;
public AbstractSpringOrchestraScope()
{
}
/**
* The advices (interceptors) which will be applied to the conversation scoped bean.
* These are applied whenever a method is invoked on the bean [1].
* <p>
* An application's spring configuration uses this method to control what advices are
* applied to beans generated from this scope. One commonly applied advice is the
* Orchestra persistence interceptor, which ensures that whenever a method on a
* conversation-scoped bean is invoked the "global persistence context" is set
* to the context for the conversation that bean is in.
* <p>
* Note [1]: the advices are only applied when the bean is invoked via its proxy. If
* invoked via the "this" pointer of the object the interceptors will not run. This
* is generally a good thing, as they are not wanted when a method on the bean invokes
* another method on the same bean. However it is bad if the bean passes "this" as a
* parameter to some other object that makes a callback on it at some later time. In
* that case, the bean must take care to pass its proxy to the remote object, not
* itself. See method ConversationUtils.getCurrentBean().
*/
public void setAdvices(Advice[] advices)
{
this.advices = advices;
}
/**
* @since 1.1
*/
protected Advice[] getAdvices()
{
return advices;
}
/**
* Configuration for a scope object to control whether "scoped proxy" objects are
* automatically wrapped around each conversation bean.
* <p>
* Automatically applying scope proxies solves a lot of tricky problems with "stale"
* beans, and should generally be used. However it does require CGLIB to be present
* in the classpath. It also can impact performance in some cases. Where this is a
* problem, this flag can turn autoproxying off. Note that the standard spring
* aop:scoped-proxy bean can then be used on individual beans to re-enable
* proxying for specific beans if desired.
* <p>
* This defaults to true.
*
* @since 1.1
*/
public void setAutoProxy(boolean state)
{
autoProxy = state;
}
/**
* Return the conversation context id.
* <p>
* Note: This conversationId is something spring requires. It has nothing to do with the Orchestra
* conversation id.
* <p>
* TODO: what does Spring use this for????
*/
public String getConversationId()
{
ConversationManager manager = ConversationManager.getInstance();
ConversationContext ctx = manager.getCurrentConversationContext();
if (ctx != null)
{
return Long.toString(ctx.getId(), 10);
}
return null;
}
/**
* This is invoked by Spring whenever someone calls getBean(name) on a bean-factory
* and the bean-definition for that bean has a scope attribute that maps to an
* instance of this class.
* <p>
* In the normal case, this method returns an internally-created proxy object
* that fetches the "real" bean every time a method is invoked on the proxy
* (see method getRealBean). This does obviously have some performance impact.
* However it is necessary when beans from one conversation are referencing beans
* from another conversation as the conversation lifetimes are not the same;
* without this proxying there are many cases where "stale" references end up
* being used. Most references to conversation-scoped objects are made via EL
* expressions, and in this case the performance impact is not significant
* relative to the overhead of EL. Note that there is one case where this
* proxying is not "transparent" to user code: if a proxied object passes a
* "this" pointer to a longer-lived object that retains that pointer then
* that reference can be "stale", as it points directly to an instance rather
* than to the proxy.
* <p>
* When the Spring aop:scoped-proxy feature is applied to conversation-scoped
* beans, then this functionality is disabled as aop:scoped-proxy has a very
* similar effect. Therefore, when this method detects that it has been invoked
* by a proxy object generated by aop:scoped-proxy then it returns the real
* object (see getRealBean) rather than another proxy. Using aop:scoped-proxy
* is somewhat less efficient than Orchestra's customised proxying.
* <p>
* And when the orchestra proxy needs to access the real object, it won't
* call this method; instead, getRealBean is called directly. See class
* ScopedBeanTargetSource.
*/
public Object get(String name, ObjectFactory objectFactory)
{
if (log.isDebugEnabled())
{
log.debug("Method get called for bean " + name);
}
if (_SpringUtils.isModifiedBeanName(name))
{
// Backwards compatibility with aop:scoped-proxy tag.
//
// The user must have included an aop:scoped-proxy within the bean definition,
// and here the proxy is firing to try to get the underlying bean. In this
// case, return a non-proxied instance of the referenced bean.
try
{
String originalBeanName = _SpringUtils.getOriginalBeanName(name);
String conversationName = getConversationNameForBean(name);
return getRealBean(conversationName, originalBeanName, objectFactory);
}
catch(RuntimeException e)
{
log.error("Exception while accessing bean '" + name + "'");
throw e;
}
}
else if (!autoProxy)
{
String conversationName = getConversationNameForBean(name);
return getRealBean(conversationName, name, objectFactory);
}
else
{
// A call has been made by the user to the Spring getBean method
// (directly, or via an EL expression). Or the bean is being fetched
// as part of spring injection into another object.
//
// In all these cases, just return a proxy.
return getProxy(name, objectFactory);
}
}
/**
* Return a CGLIB-generated proxy class for the beanclass that is
* specified by the provided beanName.
* <p>
* When any method is invoked on this proxy, it invokes method
* getRealBean on this same instance in order to locate a proper
* instance, then forwards the method call to it.
* <p>
* There is a separate proxy instance per beandef (shared across all
* instances of that bean). This instance is created when first needed,
* and cached for reuse.
*
* @since 1.1
*/
protected Object getProxy(String beanName, ObjectFactory objectFactory)
{
if (log.isDebugEnabled())
{
log.debug("getProxy called for bean " + beanName);
}
BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
String conversationName = getConversationNameForBean(beanName);
// deal with proxies required for multiple conversations.
// This is required to make the viewController scope work where proxies are
// required for each conversation a bean has been requested.
Map proxies = (Map) beanDefinition.getAttribute(ScopedBeanTargetSource.class.getName());
if (proxies == null)
{
proxies = new HashMap();
beanDefinition.setAttribute(ScopedBeanTargetSource.class.getName(), proxies);
}
Object proxy = proxies.get(conversationName);
if (proxy == null)
{
if (log.isDebugEnabled())
{
log.debug("getProxy: creating proxy for " + beanName);
}
BeanFactory beanFactory = applicationContext.getBeanFactory();
proxy = _SpringUtils.newProxy(this, conversationName, beanName, objectFactory, beanFactory);
proxies.put(conversationName, proxy);
}
// Register the proxy in req scope. The first lookup of a variable using an EL expression during a
// request will therefore take the "long" path through JSF's VariableResolver and Spring to get here.
// But later lookups of this variable in the same request find the proxy directly in the request scope.
// The proxy could potentially be placed in the session or app scope, as there is just one instance
// for this spring context, and there is normally only one spring context for a webapp. However
// using the request scope is almost as efficient and seems safer.
//
// Note that the framework adapter might not be initialised during the Spring context initialisation
// phase (ie instantiation of singletons during startup), so just skip registration in those cases.
FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
if (fa != null)
{
fa.setRequestAttribute(beanName, proxy);
}
return proxy;
}
/**
* Get a real bean instance (not a scoped-proxy).
* <p>
* The appropriate Conversation is retrieved; if it does not yet exist then
* it is created and started. The conversation name is either specified on the
* bean-definition via a custom attribute, or defaults to the bean name.
* <p>
* Then if the bean already exists in the Conversation it is returned. Otherwise
* a new instance is created, stored into the Conversation and returned.
* <p>
* When a bean is created, a proxy is actually created for it which has one or
* more AOP "advices" (ie method interceptors). The CurrentConversationAdvice class
* is always attached. Note that if the bean definition contains the aop:proxy
* tag (and most do) then the bean that spring creates is already a proxy, ie
* what is returned is a proxy of a proxy.
*
* @param conversationName
* @param beanName is the key within the conversation of the bean we are interested in.
*
* @since 1.1
*/
protected Object getRealBean(String conversationName, String beanName, ObjectFactory objectFactory)
{
if (log.isDebugEnabled())
{
log.debug("getRealBean called for bean " + beanName);
}
ConversationManager manager = ConversationManager.getInstance();
Conversation conversation;
// check if we have a conversation
synchronized(manager)
{
conversation = manager.getConversation(conversationName);
if (conversation == null)
{
// Start the conversation. This eventually results in a
// callback to the createConversation method on this class.
conversation = manager.startConversation(conversationName, this);
}
else
{
// sanity check: verify that two beans with the different scopes
// do not declare the same conversationName.
assertSameScope(beanName, conversation);
}
}
// get the conversation
notifyAccessConversation(conversation);
synchronized(conversation)
{
if (!conversation.hasAttribute(beanName))
{
Object value;
// Set the magic property that forces all proxies of this bean to be CGLIB proxies.
// It doesn't matter if we do this multiple times..
BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
beanDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
try
{
// Create the new bean. Note that this will run the
// OrchestraAdvisorBeanPostProcessor processor, which
// will cause the returned object to actually be a proxy
// with the CurrentConversationAdvice (at least) attached to it.
value = objectFactory.getObject();
}
catch(org.springframework.aop.framework.AopConfigException e)
{
throw new IllegalStateException(
"Unable to create Orchestra proxy"
+ " for bean " + beanName, e);
}
conversation.setAttribute(beanName, value);
if (value instanceof ConversationAware)
{
((ConversationAware) value).setConversation(conversation);
}
}
}
// get the bean
return conversation.getAttribute(beanName);
}
/**
* Verify that the specified conversation was created by this scope object.
*
* @param beanName is just used when generating an error message on failure.
* @param conversation is the conversation to validate.
*/
protected void assertSameScope(String beanName, Conversation conversation)
{
// Check that the conversation's factory is this one.
//
// This handles the case where two different beans declare themselves
// as belonging to the same named conversation but with different scope
// objects. Allowing that would be nasty, as the conversation
// properties (eg lifetime of access or manual) would depend upon which
// bean got created first; some other ConversationFactory would have
// created the conversation using its configured properties then
// we are now adding to that conversation a bean that really wants
// the conversation properties defined on this ConversationFactory.
//
// Ideally the conversation properties would be defined using
// the conversation name, not the scope name; this problem would
// then not exist. However that would lead to some fairly clumsy
// configuration, particularly where lots of beans without explicit
// conversationName attributes are used.
if (conversation.getFactory() != this)
{
throw new IllegalArgumentException(
"Inconsistent scope and conversation name detected for bean "
+ beanName);
}
}
protected void notifyAccessConversation(Conversation conversation)
{
}
/**
* Invoked by Spring to notify this object of the BeanFactory it is associated with.
* <p>
* This method is redundant as we also have setApplicationContext. However as this
* method (and the BeanFactoryAware interface on this class) were present in release
* 1.0 this needs to be kept for backwards compatibility.
*/
public void setBeanFactory(BeanFactory beanFactory) throws BeansException
{
}
/**
* Register any BeanPostProcessors that are needed by this scope.
* <p>
* This is an alternative to requiring users to also add an orchestra BeanPostProcessor element
* to their xml configuration file manually.
* <p>
* When a bean <i>instance</i> is created by Spring, it always runs every single BeanPostProcessor
* that has been registered with it.
*
* @since 1.1
*/
public void defineBeanPostProcessors(ConfigurableListableBeanFactory cbf) throws BeansException
{
if (!cbf.containsSingleton(POST_PROCESSOR_BEAN_NAME))
{
BeanPostProcessor processor = new OrchestraAdvisorBeanPostProcessor(applicationContext);
// Adding the bean to the singletons set causes it to be picked up by the standard
// AbstractApplicationContext.RegisterBeanPostProcessors method; that calls
// getBeanNamesForType(BeanPostProcessor.class, ...) which finds stuff in the
// singleton map even when there is no actual BeanDefinition for it.
//
// We cannot call cbf.addBeanPostProcessor even if we want to, as the singleton
// registration will be added again, making the processor run twice on each bean.
// And we need the singleton registration in order to avoid registering this once
// for each scope object defined in spring.
cbf.registerSingleton(POST_PROCESSOR_BEAN_NAME, processor);
}
}
/**
* Get the conversation for the given beanName.
* Returns null if the conversation does not exist.
*/
protected Conversation getConversationForBean(String beanDefName)
{
ConversationManager manager = ConversationManager.getInstance();
String conversationName = getConversationNameForBean(beanDefName);
Conversation conversation = manager.getConversation(conversationName);
return conversation;
}
/**
* Get the conversation-name for bean instances created using the specified
* bean definition.
*/
public String getConversationNameForBean(String beanName)
{
if (applicationContext == null)
{
throw new IllegalStateException("Null application context");
}
// Look up the definition with the specified name.
BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
if (ScopedProxyFactoryBean.class.getName().equals(beanDefinition.getBeanClassName()))
{
// Handle unusual case.
//
// The user bean must have been defined like this:
// <bean name="foo" class="example.Foo">
// <....>
// <aop:scopedProxy/>
// </bean>
// In this case, Spring's ScopedProxyUtils class creates two BeanDefinition objects, one
// with name "foo" that creates a proxy object, and one with name "scopedTarget.foo"
// that actually defines the bean of type example.Foo.
//
// So what we do here is find the renamed bean definition and look there.
//
// This case does not occur when this method is invoked from within this class; the
// spring scope-related callbacks always deal with the beandef that is scoped to
// this scope - which is the original (though renamed) beandef.
beanName = _SpringUtils.getModifiedBeanName(beanName);
beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName); // NON-NLS
}
String convName = getExplicitConversationName(beanDefinition);
if (convName == null)
{
// The beanname might have been of form "scopedTarget.foo" (esp from registerDestructionCallback).
// But in this case, the conversation name will just be "foo", so strip the prefix off.
//
// Note that this does happen quite often for calls from within this class when aop:scoped-proxy
// is being used (which is not recommended but is supported).
convName = _SpringUtils.getOriginalBeanName(beanName);
}
return convName;
}
/**
* Return the explicit conversation name for this bean definition, or null if there is none.
* <p>
* This is a separate method so that subclasses can determine conversation names via alternate methods.
* In particular, a subclass may want to look for an annotation on the class specified by the definition.
*
* @since 1.1
*/
protected String getExplicitConversationName(BeanDefinition beanDef)
{
String attr = (String) beanDef.getAttribute(
BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
return attr;
}
/**
* Strip off any Spring namespace (eg scopedTarget).
* <p>
* This method will simply strip off anything before the first dot.
*
* @deprecated Should not be necessary in user code.
*/
protected String buildBeanName(String name)
{
if (name == null)
{
return null;
}
int pos = name.indexOf('.');
if (pos < 0)
{
return name;
}
return name.substring(pos + 1);
}
public Object remove(String name)
{
throw new UnsupportedOperationException();
}
/**
* Add the given runnable wrapped within an
* {@link org.apache.myfaces.orchestra.conversation.ConversationBindingListener} to
* the conversation map.
* <p>
* This ensures it will be called during conversation destroy.
* <p>
* Spring calls this method whenever a bean in this scope is created, if that bean
* has a "destroy method". Note however that it appears that it can also call it even
* for beans that do not have a destroy method when there is a "destruction aware"
* BeanPostProcessor attached to the context (spring version 2.5 at least).
* <p>
* When an aop:scoped-proxy has been used inside the bean, then the "new" definition
* does not have any scope attribute, so orchestra is not invoked for it. However
* the "renamed" bean does, and so this is called.
*/
public void registerDestructionCallback(String name, final Runnable runnable)
{
if (log.isDebugEnabled())
{
log.debug("registerDestructionCallback for [" + name + "]");
}
Conversation conversation = getConversationForBean(name);
if (conversation == null)
{
// This should never happen because this should only be called after the bean
// instance has been created via scope.getBean, which always creates the
// conversation for the bean.
throw new IllegalStateException("No conversation for bean [" + name + "]");
}
if (runnable == null)
{
throw new IllegalStateException("No runnable object for bean [" + name + "]");
}
// Add an object to the conversation as a bean so that when the conversation is removed
// its valueUnbound method will be called. However we never need to retrieve this object
// from the context by name, so use a totally unique name as the bean key.
conversation.setAttribute(
runnable.getClass().getName() + "@" + System.identityHashCode(runnable),
new ConversationBindingListener()
{
public void valueBound(ConversationBindingEvent event)
{
}
public void valueUnbound(ConversationBindingEvent event)
{
runnable.run();
}
}
);
}
/**
* Get an ApplicationContext injected by Spring. See ApplicationContextAware interface.
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
if (!(applicationContext instanceof ConfigurableApplicationContext))
{
throw new IllegalArgumentException("a ConfigurableApplicationContext is required");
}
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
defineBeanPostProcessors(this.applicationContext.getBeanFactory());
}
/**
* @since 1.2
*/
protected ConfigurableApplicationContext getApplicationContext()
{
return applicationContext;
}
}