/*
* 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.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.ConversationFactory;
import org.apache.myfaces.orchestra.conversation.ConversationManager;
import org.apache.myfaces.orchestra.conversation.CurrentConversationAdvice;
import org.springframework.aop.framework.ProxyFactory;
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.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
/**
* 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 ConfigurableApplicationContext applicationContext;
private Advice[] advices;
public AbstractSpringOrchestraScope()
{
}
/**
* The advices (interceptors) which will be applied to the conversation scoped bean.
*/
public void setAdvices(Advice[] advices)
{
this.advices = advices;
}
/**
* Return the conversation context id.
* <p>
* Note: This conversationId is something spring requires. It has nothing to do with the Orchestra
* conversation id.
*/
public String getConversationId()
{
ConversationManager manager = ConversationManager.getInstance();
if (manager.hasConversationContext())
{
return Long.toString(manager.getConversationContextId().longValue(), 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>
* First, the appropriate ConversationContext is retrieved.
* <p>
* Second, 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 then 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.
*/
public Object get(String name, ObjectFactory objectFactory)
{
name = buildBeanName(name);
return getBean(name, objectFactory);
}
/** See method get(name, objectFactory). */
protected Object getBean(String beanName, ObjectFactory objectFactory)
{
String conversationName = getConversationNameForBean(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
{
assertSameScope(beanName, conversation);
}
}
// get the conversation
notifyAccessConversation(conversation);
synchronized(conversation)
{
if (!conversation.hasAttribute(beanName))
{
// create the bean (if not already done)
Object value = objectFactory.getObject();
ProxyFactory factory = new ProxyFactory(value);
factory.setProxyTargetClass(true);
factory.addAdvice(new CurrentConversationAdvice(conversation, beanName));
if (advices != null && advices.length > 0)
{
for (int i = 0; i < advices.length; i++)
{
factory.addAdvice(advices[i]);
}
}
value = factory.getProxy();
conversation.setAttribute(beanName, value);
}
}
// get the bean
return conversation.getAttribute(beanName);
}
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 conversation but with different scope
// objects. Allowing that would be nasty as the conversation
// properties (eg flash 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)
{
}
/**
* Set the {@link org.apache.myfaces.orchestra.conversation.Conversation} object to the bean if it implements the
* {@link org.apache.myfaces.orchestra.conversation.ConversationAware} interface.
*/
public void setBeanFactory(BeanFactory beanFactory) throws BeansException
{
((ConfigurableBeanFactory) beanFactory).addBeanPostProcessor(
new BeanPostProcessor()
{
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
{
if (bean instanceof ConversationAware)
{
Conversation conversation = getConversationForBean(beanName);
((ConversationAware) bean).setConversation(conversation);
}
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
{
return bean;
}
}
);
}
/**
* Get the conversation for the given beanName.
*/
protected Conversation getConversationForBean(String beanName)
{
ConversationManager manager = ConversationManager.getInstance();
Conversation conversation = manager.getConversation(getConversationNameForBean(beanName));
return conversation;
}
/**
* Get the conversation name associated with the beanName.
*/
protected String getConversationNameForBean(String beanName)
{
if (applicationContext != null)
{
BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
if (ScopedProxyFactoryBean.class.getName().equals(beanDefinition.getBeanClassName()))
{
// bad hack to get access to the real definition
// TODO: is there a better way to do this?
beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(_SpringUtils.getAlternateBeanName(beanName)); // NON-NLS
}
if (beanDefinition.hasAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE))
{
return (String) beanDefinition.getAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
}
}
return beanName;
}
/**
* Strip off any Spring namespace (eg scopedTarget).
* <p>
* This method will simply strip off anything before the first dot.
*/
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.
*/
public void registerDestructionCallback(String name, final Runnable runnable)
{
Conversation conversation = getConversationForBean(name);
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;
}
}