View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.orchestra.conversation.spring;
21  
22  import org.aopalliance.aop.Advice;
23  import org.apache.myfaces.orchestra.conversation.Conversation;
24  import org.apache.myfaces.orchestra.conversation.ConversationAware;
25  import org.apache.myfaces.orchestra.conversation.ConversationBindingEvent;
26  import org.apache.myfaces.orchestra.conversation.ConversationBindingListener;
27  import org.apache.myfaces.orchestra.conversation.ConversationFactory;
28  import org.apache.myfaces.orchestra.conversation.ConversationManager;
29  import org.apache.myfaces.orchestra.conversation.CurrentConversationAdvice;
30  import org.springframework.aop.framework.ProxyFactory;
31  import org.springframework.aop.scope.ScopedProxyFactoryBean;
32  import org.springframework.beans.BeansException;
33  import org.springframework.beans.factory.BeanFactory;
34  import org.springframework.beans.factory.BeanFactoryAware;
35  import org.springframework.beans.factory.ObjectFactory;
36  import org.springframework.beans.factory.config.BeanDefinition;
37  import org.springframework.beans.factory.config.BeanPostProcessor;
38  import org.springframework.beans.factory.config.ConfigurableBeanFactory;
39  import org.springframework.beans.factory.config.Scope;
40  import org.springframework.context.ApplicationContext;
41  import org.springframework.context.ApplicationContextAware;
42  import org.springframework.context.ConfigurableApplicationContext;
43  
44  /***
45   * Abstract basis class for all the Orchestra scopes.
46   * <p>
47   * A scope object has two quite different roles:
48   * <ol>
49   * <li>It handles the lookup of beans in a scope, and creates them if needed</li>
50   * <li>It handles the creation of Conversation objects, using the spring properties
51   * configured on the scope object.</li>
52   * </ol>
53   * <p>
54   * This base class handles item 1 above, and leaves item 2 to a subclass. The
55   * declaration of interface ConversationFactory needs to be on this class, however,
56   * as the createBean method needs to invoke it. 
57   */
58  public abstract class AbstractSpringOrchestraScope implements ConversationFactory, 
59  	Scope, BeanFactoryAware, ApplicationContextAware
60  {
61  	private ConfigurableApplicationContext applicationContext;
62  	private Advice[] advices;
63  
64  	public AbstractSpringOrchestraScope()
65  	{
66  	}
67  
68  	/***
69  	 * The advices (interceptors) which will be applied to the conversation scoped bean.
70  	 */
71  	public void setAdvices(Advice[] advices)
72  	{
73  		this.advices = advices;
74  	}
75  
76  	/***
77  	 * Return the conversation context id.
78  	 * <p>
79  	 * Note: This conversationId is something spring requires. It has nothing to do with the Orchestra
80  	 * conversation id.
81  	 */
82  	public String getConversationId()
83  	{
84  		ConversationManager manager = ConversationManager.getInstance();
85  		if (manager.hasConversationContext())
86  		{
87  			return Long.toString(manager.getConversationContextId().longValue(), 10);
88  		}
89  
90  		return null;
91  	}
92  
93  	/***
94  	 * This is invoked by Spring whenever someone calls getBean(name) on a bean-factory
95  	 * and the bean-definition for that bean has a scope attribute that maps to an
96  	 * instance of this class.
97  	 * <p>
98  	 * First, the appropriate ConversationContext is retrieved.
99  	 * <p>
100 	 * Second, the appropriate Conversation is retrieved; if it does not yet exist then
101 	 * it is created and started. The conversation name is either specified on the
102 	 * bean-definition via a custom attribute, or defaults to the bean name.
103 	 * <p>
104 	 * Then if the bean already exists in the Conversation then it is returned. Otherwise
105 	 * a new instance is created, stored into the Conversation and returned.
106 	 * <p>
107 	 * When a bean is created, a proxy is actually created for it which has one or
108 	 * more AOP "advices" (ie method interceptors). The CurrentConversationAdvice class
109 	 * is always attached. Note that if the bean definition contains the aop:proxy
110 	 * tag (and most do) then the bean that spring creates is already a proxy, ie
111 	 * what is returned is a proxy of a proxy.
112 	 */
113 	public Object get(String name, ObjectFactory objectFactory)
114 	{
115 		name = buildBeanName(name);
116 
117 		return getBean(name, objectFactory);
118 	}
119 
120 	/*** See method get(name, objectFactory). */
121 	protected Object getBean(String beanName, ObjectFactory objectFactory)
122 	{
123 		String conversationName = getConversationNameForBean(beanName);
124 
125 		ConversationManager manager = ConversationManager.getInstance();
126 		Conversation conversation;
127 
128 		// check if we have a conversation
129 		synchronized(manager)
130 		{
131 			conversation = manager.getConversation(conversationName);
132 			if (conversation == null)
133 			{
134 				// Start the conversation. This eventually results in a 
135 				// callback to the createConversation method on this class.
136 				conversation = manager.startConversation(conversationName, this);
137 			}
138 			else
139 			{
140                 assertSameScope(beanName, conversation);
141             }
142 		}
143 
144 		// get the conversation
145 		notifyAccessConversation(conversation);
146 		synchronized(conversation)
147 		{
148 			if (!conversation.hasAttribute(beanName))
149 			{
150 				// create the bean (if not already done)
151 				Object value = objectFactory.getObject();
152 
153 				ProxyFactory factory = new ProxyFactory(value);
154 				factory.setProxyTargetClass(true);
155 				factory.addAdvice(new CurrentConversationAdvice(conversation, beanName));
156 
157 				if (advices != null && advices.length > 0)
158 				{
159 					for (int i = 0; i < advices.length; i++)
160 					{
161 						factory.addAdvice(advices[i]);
162 					}
163 				}
164 
165 				value = factory.getProxy();
166 
167 				conversation.setAttribute(beanName, value);
168 			}
169 		}
170 
171 		// get the bean
172 		return conversation.getAttribute(beanName);
173 	}
174 
175     protected void assertSameScope(String beanName, Conversation conversation)
176     {
177         // Check that the conversation's factory is this one.
178         //
179         // This handles the case where two different beans declare themselves
180         // as belonging to the same conversation but with different scope
181         // objects. Allowing that would be nasty as the conversation
182         // properties (eg flash or manual) would depend upon which bean
183         // got created first; some other ConversationFactory would have
184         // created the conversation using its configured properties then
185         // we are now adding to that conversation a bean that really wants
186         // the conversation properties defined on this ConversationFactory.
187         //
188         // Ideally the conversation properties would be defined using
189         // the conversation name, not the scope name; this problem would
190         // then not exist. However that would lead to some fairly clumsy
191         // configuration, particularly where lots of beans without explicit
192         // conversationName attributes are used.
193 
194         if (conversation.getFactory() != this)
195         {
196             throw new IllegalArgumentException(
197                 "Inconsistent scope and conversation name detected for bean "
198                     + beanName);
199         }
200     }
201 
202     protected void notifyAccessConversation(Conversation conversation)
203 	{
204 	}
205 
206 	/***
207 	 * Set the {@link org.apache.myfaces.orchestra.conversation.Conversation} object to the bean if it implements the
208 	 * {@link org.apache.myfaces.orchestra.conversation.ConversationAware} interface.
209 	 */
210 	public void setBeanFactory(BeanFactory beanFactory) throws BeansException
211 	{
212 		((ConfigurableBeanFactory) beanFactory).addBeanPostProcessor(
213 			new BeanPostProcessor()
214 			{
215 				public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
216 				{
217 					if (bean instanceof ConversationAware)
218 					{
219 						Conversation conversation = getConversationForBean(beanName);
220 
221 						((ConversationAware) bean).setConversation(conversation);
222 					}
223 
224 					return bean;
225 				}
226 
227 				public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
228 				{
229 					return bean;
230 				}
231 			}
232 		);
233 	}
234 
235 	/***
236 	 * Get the conversation for the given beanName.
237 	 */
238 	protected Conversation getConversationForBean(String beanName)
239 	{
240 		ConversationManager manager = ConversationManager.getInstance();
241 		Conversation conversation = manager.getConversation(getConversationNameForBean(beanName));
242 		return conversation;
243 	}
244 
245 	/***
246 	 * Get the conversation name associated with the beanName.
247 	 */
248 	protected String getConversationNameForBean(String beanName)
249 	{
250 		if (applicationContext != null)
251 		{
252 			BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
253 			if (ScopedProxyFactoryBean.class.getName().equals(beanDefinition.getBeanClassName()))
254 			{
255 				// bad hack to get access to the real definition
256 				// TODO: is there a better way to do this?
257 				beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(_SpringUtils.getAlternateBeanName(beanName)); // NON-NLS
258 			}
259 			if (beanDefinition.hasAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE))
260 			{
261 				return (String) beanDefinition.getAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
262 			}
263 		}
264 
265 		return beanName;
266 	}
267 
268 	/***
269 	 * Strip off any Spring namespace (eg scopedTarget).
270 	 * <p>
271 	 * This method will simply strip off anything before the first dot.
272 	 */
273 	protected String buildBeanName(String name)
274 	{
275 		if (name == null)
276 		{
277 			return null;
278 		}
279 
280 		int pos = name.indexOf('.');
281 		if (pos < 0)
282 		{
283 			return name;
284 		}
285 
286 		return name.substring(pos + 1);
287 	}
288 
289 	public Object remove(String name)
290 	{
291 		throw new UnsupportedOperationException();
292 	}
293 
294 	/***
295 	 * Add the given runnable wrapped within an
296 	 * {@link org.apache.myfaces.orchestra.conversation.ConversationBindingListener} to
297 	 * the conversation map.
298 	 * <p>
299 	 * This ensures it will be called during conversation destroy.
300 	 */
301 	public void registerDestructionCallback(String name, final Runnable runnable)
302 	{
303 		Conversation conversation = getConversationForBean(name);
304 		conversation.setAttribute(
305 			runnable.getClass().getName() + "@" + System.identityHashCode(runnable),
306 			new ConversationBindingListener()
307 			{
308 				public void valueBound(ConversationBindingEvent event)
309 				{
310 				}
311 
312 				public void valueUnbound(ConversationBindingEvent event)
313 				{
314 					runnable.run();
315 				}
316 			}
317 		);
318 	}
319 
320 	/***
321 	 * Get an ApplicationContext injected by Spring. See ApplicationContextAware interface.
322 	 */
323 	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
324 	{
325 		if (!(applicationContext instanceof ConfigurableApplicationContext))
326 		{
327 			throw new IllegalArgumentException("a ConfigurableApplicationContext is required");
328 		}
329 
330 		this.applicationContext = (ConfigurableApplicationContext) applicationContext;
331 	}
332 }