From f812cd748ea4a453627efca00a19a539541dd9c0 Mon Sep 17 00:00:00 2001 From: Micha Kiener Date: Tue, 12 Apr 2011 13:21:18 +0000 Subject: [PATCH] SPR-6416, initial commit for the conversation management --- .../DestructionAwareAttributeHolder.java | 202 +++++++++++++ .../conversation/Conversation.java | 146 ++++++++++ .../conversation/ConversationManager.java | 109 +++++++ .../conversation/ConversationType.java | 45 +++ ...AnnotationConversationAttributeSource.java | 105 +++++++ .../annotation/BeginConversation.java | 56 ++++ .../BeginConversationAnnotationParser.java | 40 +++ .../ConversationAnnotationParser.java | 36 +++ .../annotation/EndConversation.java | 47 +++ .../EndConversationAnnotationParser.java | 39 +++ ...oryConversationAttributeSourceAdvisor.java | 55 ++++ .../interceptor/ConversationAttribute.java | 77 +++++ .../ConversationAttributeSource.java | 41 +++ .../ConversationAttributeSourcePointcut.java | 41 +++ .../interceptor/ConversationInterceptor.java | 95 ++++++ .../DefaultConversationAttribute.java | 76 +++++ .../AbstractConversationRepository.java | 125 ++++++++ .../manager/ConversationRepository.java | 85 ++++++ .../manager/ConversationResolver.java | 49 ++++ .../manager/DefaultConversation.java | 272 ++++++++++++++++++ .../manager/DefaultConversationManager.java | 178 ++++++++++++ .../LocalTransientConversationRepository.java | 69 +++++ .../manager/MutableConversation.java | 93 ++++++ .../ThreadLocalConversationResolver.java | 46 +++ .../conversation/scope/ConversationScope.java | 109 +++++++ org.springframework.context/template.mf | 1 + .../SessionBasedConversationRepository.java | 111 +++++++ .../WebAwareConversationScope.java | 45 +++ org.springframework.web/template.mf | 2 + 29 files changed, 2395 insertions(+) create mode 100644 org.springframework.beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareAttributeHolder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/Conversation.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/ConversationManager.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/ConversationType.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/annotation/AnnotationConversationAttributeSource.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversation.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversationAnnotationParser.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/annotation/ConversationAnnotationParser.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversation.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversationAnnotationParser.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/interceptor/BeanFactoryConversationAttributeSourceAdvisor.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttribute.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSource.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSourcePointcut.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationInterceptor.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/interceptor/DefaultConversationAttribute.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/AbstractConversationRepository.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationRepository.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationResolver.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversation.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversationManager.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/LocalTransientConversationRepository.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/MutableConversation.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/manager/ThreadLocalConversationResolver.java create mode 100644 org.springframework.context/src/main/java/org/springframework/conversation/scope/ConversationScope.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/conversation/SessionBasedConversationRepository.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/conversation/WebAwareConversationScope.java diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareAttributeHolder.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareAttributeHolder.java new file mode 100644 index 00000000000..ae7276fb33e --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareAttributeHolder.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.beans.factory.config; + +import java.io.Serializable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A container object holding a map of attributes and optionally destruction callbacks. The callbacks will be invoked, + * if an attribute is being removed or if the holder is cleaned out. + * + * @author Micha Kiener + * @since 3.1 + */ +public class DestructionAwareAttributeHolder implements Serializable { + + /** The map containing the registered attributes. */ + private final Map attributes = new ConcurrentHashMap(); + + /** + * The optional map having any destruction callbacks registered using the + * name of the bean as the key. + */ + private Map registeredDestructionCallbacks; + + + + /** + * Returns the map representation of the registered attributes directly. Be + * aware to synchronize any invocations to it on the map object itself to + * avoid concurrent modification exceptions. + * + * @return the attributes as a map representation + */ + public Map getAttributeMap() { + return attributes; + } + + /** + * Returns the attribute having the specified name, if available, + * null otherwise. + * + * @param name + * the name of the attribute to be returned + * @return the attribute value or null if not available + */ + @SuppressWarnings("unchecked") + public Object getAttribute(String name) { + return attributes.get(name); + } + + /** + * Puts the given object with the specified name as an attribute to the + * underlying map. + * + * @param name + * the name of the attribute + * @param value + * the value to be stored + * @return any previously object stored under the same name, if any, + * null otherwise + */ + @SuppressWarnings("unchecked") + public Object setAttribute(String name, Object value) { + return attributes.put(name, value); + } + + /** + * Remove the object with the given name from the underlying + * scope. + *

+ * Returns null if no object was found; otherwise returns the + * removed Object. + *

+ * Note that an implementation should also remove a registered destruction + * callback for the specified object, if any. It does, however, not + * need to execute a registered destruction callback in this case, + * since the object will be destroyed by the caller (if appropriate). + *

+ * Note: This is an optional operation. Implementations may throw + * {@link UnsupportedOperationException} if they do not support explicitly + * removing an object. + * + * @param name + * the name of the object to remove + * @return the removed object, or null if no object was present + * @see #registerDestructionCallback + */ + @SuppressWarnings("unchecked") + public Object removeAttribute(String name) { + Object value = attributes.remove(name); + + // check for a destruction callback to be invoked + Runnable callback = getDestructionCallback(name, true); + if (callback != null) { + callback.run(); + } + + return value; + } + + /** + * Clears the map by removing all registered attribute values and invokes + * every destruction callback registered. + */ + public void clear() { + synchronized (this) { + // step through the attribute map and invoke destruction callbacks, + // if any + if (registeredDestructionCallbacks != null) { + for (Runnable runnable : registeredDestructionCallbacks.values()) { + runnable.run(); + } + + registeredDestructionCallbacks.clear(); + } + } + + // clear out the registered attribute map + attributes.clear(); + } + + /** + * Register a callback to be executed on destruction of the specified object + * in the scope (or at destruction of the entire scope, if the scope does + * not destroy individual objects but rather only terminates in its + * entirety). + *

+ * Note: This is an optional operation. This method will only be + * called for scoped beans with actual destruction configuration + * (DisposableBean, destroy-method, DestructionAwareBeanPostProcessor). + * Implementations should do their best to execute a given callback at the + * appropriate time. If such a callback is not supported by the underlying + * runtime environment at all, the callback must be ignored and a + * corresponding warning should be logged. + *

+ * Note that 'destruction' refers to to automatic destruction of the object + * as part of the scope's own lifecycle, not to the individual scoped object + * having been explicitly removed by the application. If a scoped object + * gets removed via this facade's {@link #removeAttribute(String)} method, + * any registered destruction callback should be removed as well, assuming + * that the removed object will be reused or manually destroyed. + * + * @param name + * the name of the object to execute the destruction callback for + * @param callback + * the destruction callback to be executed. Note that the + * passed-in Runnable will never throw an exception, so it can + * safely be executed without an enclosing try-catch block. + * Furthermore, the Runnable will usually be serializable, + * provided that its target object is serializable as well. + * @see org.springframework.beans.factory.DisposableBean + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getDestroyMethodName() + * @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor + */ + public void registerDestructionCallback(String name, Runnable callback) { + if (registeredDestructionCallbacks == null) { + registeredDestructionCallbacks = new ConcurrentHashMap(); + } + + registeredDestructionCallbacks.put(name, callback); + } + + /** + * Returns the destruction callback, if any registered for the attribute + * with the given name or null if no such callback was + * registered. + * + * @param name + * the name of the registered callback requested + * @param remove + * true, if the callback should be removed after + * this call, false, if it stays + * @return the callback, if found, null otherwise + */ + public Runnable getDestructionCallback(String name, boolean remove) { + if (registeredDestructionCallbacks == null) { + return null; + } + + if (remove) { + return registeredDestructionCallbacks.remove(name); + } + + return registeredDestructionCallbacks.get(name); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/Conversation.java b/org.springframework.context/src/main/java/org/springframework/conversation/Conversation.java new file mode 100644 index 00000000000..c464e5d81ea --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/Conversation.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation; + +import java.util.List; + +/** + * The interface for a conversation object being managed by the {@link ConversationManager} and created, stored and + * removed by the {@link org.springframework.conversation.manager.ConversationRepository}.
+ * The conversation object is most likely never used directly but rather indirectly through the + * {@link org.springframework.conversation.scope.ConversationScope}. It supports fine grained access to the conversation + * container for storing and retrieving attributes, access the conversation hierarchy or manage the timeout behavior + * of the conversation. + * + * @author Micha Kiener + * @since 3.1 + */ +public interface Conversation { + + /** + * Returns the id of this conversation which must be unique within the scope it is used to identify the conversation + * object. The id is set by the {@link org.springframework.conversation.manager.ConversationRepository} and most + * likely be used by the {@link org.springframework.conversation.manager.ConversationResolver} in order to manage + * the current conversation. + * + * @return the id of this conversation + */ + String getId(); + + /** + * Stores the given value in this conversation using the specified name. If this state already contains a value + * attached to the given name, it is returned, null otherwise.
This method stores the attribute + * within this conversation so it will be available through this and all nested conversations. + * + * @param name the name of the value to be stored in this conversation + * @param value the value to be stored + * @return the old value attached to the same name, if any, null otherwise + */ + Object setAttribute(String name, Object value); + + /** + * Returns the value attached to the given name, if any previously registered, null otherwise.
+ * Returns the attribute stored with the given name within this conversation or any within the path through its parent + * to the top level root conversation. If this is a nested, isolated conversation, attributes are only being resolved + * within this conversation, not from its parent. + * + * @param name the name of the value to be retrieved + * @return the value, if available in the current state, null otherwise + */ + Object getAttribute(String name); + + /** + * Removes the value in the current conversation having the given name and returns it, if found and removed, + * null otherwise.
Removes the attribute from this specific conversation, does not remove it, if + * found within its parent. + * + * @param name the name of the value to be removed from this conversation + * @return the removed value, if found, null otherwise + */ + Object removeAttribute(String name); + + /** + * Returns the top level root conversation, if this is a nested conversation or this conversation, if it is the top + * level root conversation. This method never returns null. + * + * @return the root conversation (top level conversation) + */ + Conversation getRoot(); + + /** + * Returns the parent conversation, if this is a nested conversation, null otherwise. + * + * @return the parent conversation, if any, null otherwise + */ + Conversation getParent(); + + /** + * Returns a list of child conversations, if any, an empty list otherwise, must never return null. + * + * @return a list of child conversations (may be empty, never null) + */ + List getChildren(); + + /** + * Returns true, if this is a nested conversation and hence {@link #getParent()} will returns a non-null + * value. + * + * @return true, if this is a nested conversation, false otherwise + */ + boolean isNested(); + + /** + * Returns true, if this is a nested, isolated conversation so that it does not inherit the state from its + * parent but rather has its own state. See {@link ConversationType#ISOLATED} for more details. + * + * @return true, if this is a nested, isolated conversation + */ + boolean isIsolated(); + + /** + * Returns the timestamp in milliseconds this conversation has been created. + * + * @return the creation timestamp in millis + */ + long getCreationTime(); + + /** + * Returns the timestamp in milliseconds this conversation was last accessed (usually through a {@link + * #getAttribute(String)}, {@link #setAttribute(String, Object)} or {@link #removeAttribute(String)} access). + * + * @return the system time in milliseconds for the last access of this conversation + */ + long getLastAccessedTime(); + + /** + * Returns the timeout of this conversation object in seconds. A value of 0 stands for no timeout. + * The timeout is usually managed on the root conversation object and will be returned regardless of the hierarchy + * of this conversation. + * + * @return the timeout in seconds if any, 0 otherwise + */ + int getTimeout(); + + /** + * Set the timeout of this conversation hierarchy in seconds. A value of 0 stands for no timeout. + * Regardless of the hierarchy of this conversation, a timeout is always set on the top root conversation and is + * valid for all conversations within the same hierarchy. + * + * @param timeout the timeout in seconds to set, 0 for no timeout + */ + void setTimeout(int timeout); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/ConversationManager.java b/org.springframework.context/src/main/java/org/springframework/conversation/ConversationManager.java new file mode 100644 index 00000000000..983876da3c3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/ConversationManager.java @@ -0,0 +1,109 @@ + +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation; + +/** + *

+ * A conversation manager is used to manage conversations, most of all, the current conversation. It is used by + * the advice behind the conversation annotations {@link org.springframework.conversation.annotation.BeginConversation} + * and {@link org.springframework.conversation.annotation.EndConversation} in order to start and end conversations. + *

+ *

+ * A conversation manager uses a {@link org.springframework.conversation.manager.ConversationRepository} to create, + * store and remove conversation objects and a {@link org.springframework.conversation.manager.ConversationResolver} + * to set and remove the current conversation id. + *

+ *

+ * A conversation manager might be used manually in order to start and end conversations manually. + *

+ *

+ * Conversations are a good way to scope beans and attributes depending on business logic boundary rather than a + * technical boundary of a scope like session, request etc. Usually a conversation boundary is defined by the starting + * point of a use case and ended accordingly or in other words a conversation defines the boundary for a unit of + * work.

+ * + * A conversation is either implicitly started upon the first request of a conversation scoped bean or it is + * explicitly started by using the conversation manager manually or by placing the begin conversation on a method.
+ * The same applies for ending conversations as they are either implicitly ended by starting a new one or if the + * timeout of a conversation is reached or they are ended explicitly by placing the end conversation annotation or + * using the conversation manager manually. + *

+ *

+ * Conversations might have child conversations which are either nested and hence will inherit the state of their + * parent or they are isolated by having its own state and hence being independent from its parent. + *

+ *

+ * Extending the conversation management
+ * The conversation management ships with different out-of-the box implementations but is easy to extend. + * To extend the storage mechanism of conversations, the {@link org.springframework.conversation.manager.ConversationRepository} + * and maybe the {@link org.springframework.conversation.manager.DefaultConversation} have to be extended or + * overwritten to support the desired behavior.
+ * To change the behavior where the current conversation is stored, either overwrite the + * {@link org.springframework.conversation.manager.ConversationResolver} or make sure the current conversation id + * is being resolved, stored and removed within the default {@link org.springframework.conversation.manager.ThreadLocalConversationResolver}. + *

+ * + * @author Micha Kiener + * @since 3.1 + */ +public interface ConversationManager { + + /** + * Returns the current conversation and creates a new one, if there is currently no active conversation yet. + * Internally, the manager will use the {@link org.springframework.conversation.manager.ConversationResolver} + * to resolve the current conversation id and the {@link org.springframework.conversation.manager.ConversationRepository} + * to load the conversation object being returned. + * + * @return the current conversation, never null, will create a new conversation, if no one existing + */ + Conversation getCurrentConversation(); + + /** + * Returns the current conversation, if existing or creates a new one, if currently no active conversation available + * and createIfNotExisting is specified as true. + * + * @param createNewIfNotExisting true, if a new conversation should be created, if there is currently + * no active conversation in place, false to return null, if no current conversation active + * @return the current conversation or null, if no current conversation available and + * createIfNotExisting is set as false + */ + Conversation getCurrentConversation(boolean createNewIfNotExisting); + + /** + * Creates a new conversation according the given conversationType and makes it the current active + * conversation. See {@link ConversationType} for more detailed information about the different conversation + * creation types available.
+ * If {@link ConversationType#NEW} is specified, the current conversation will automatically be ended + * + * @param conversationType the type used to start a new conversation + * @return the newly created conversation + */ + Conversation beginConversation(ConversationType conversationType); + + /** + * Ends the current conversation, if any. If root is true, the whole conversation + * hierarchy is ended and there will no current conversation be active afterwards. If root is + * false, the current conversation is ended and if it is a nested one, its parent is made the + * current conversation. + * + * @param root true to end the whole current conversation hierarchy or false to just + * end the current conversation + */ + void endCurrentConversation(boolean root); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/ConversationType.java b/org.springframework.context/src/main/java/org/springframework/conversation/ConversationType.java new file mode 100644 index 00000000000..c08e166ba93 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/ConversationType.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation; + +/** + * The conversation type is used while starting a new conversation and declares how the conversation manager + * should create and start it as well how to end the current conversation, if any. + * + * @author Micha Kiener + * @since 3.1 + */ +public enum ConversationType { + /** + * The type NEW creates a new root conversation and will end a current one, if any. + */ + NEW, + + /** + * The type NESTED will create a new conversation and add it as a child conversation to the current one, + * if available. If there is no current conversation, this type is the same as NEW. + * A nested conversation will inherit the state from its parent. + */ + NESTED, + + /** + * The type ISOLATED is basically the same as NESTED but will isolate the state from its parent. While a + * nested conversation will inherit the state from its parent, an isolated one does not but rather has its + * own state. + */ + ISOLATED +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/annotation/AnnotationConversationAttributeSource.java b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/AnnotationConversationAttributeSource.java new file mode 100644 index 00000000000..1efcd176f07 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/AnnotationConversationAttributeSource.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.annotation; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aop.support.AopUtils; +import org.springframework.conversation.interceptor.ConversationAttribute; +import org.springframework.conversation.interceptor.ConversationAttributeSource; +import org.springframework.util.Assert; + +/** + * ConversationAttributeSource implementation that uses annotation meta-data to provide a ConversationAttribute instance + * for a particular method. + * + * @author Agim Emruli + */ +public class AnnotationConversationAttributeSource implements ConversationAttributeSource { + + private final Set conversationAnnotationParsers; + + /** + * Default constructor that uses the Spring standard ConversationAnnotationParser instances to parse the annotation + * meta-data. + * + * @see org.springframework.conversation.annotation.BeginConversationAnnotationParser + * @see org.springframework.conversation.annotation.EndConversationAnnotationParser + * @see org.springframework.conversation.annotation.ConversationAnnotationParser + */ + public AnnotationConversationAttributeSource() { + Set defaultParsers = new LinkedHashSet(); + Collections + .addAll(defaultParsers, new BeginConversationAnnotationParser(), new EndConversationAnnotationParser()); + conversationAnnotationParsers = defaultParsers; + } + + /** + * Constructor that uses the custom ConversationAnnotationParser to parse the annotation meta-data. + * + * @param conversationAnnotationParsers The ConversationAnnotationParser instance that will be used to parse annotation + * meta-data + */ + public AnnotationConversationAttributeSource(ConversationAnnotationParser conversationAnnotationParsers) { + this(Collections.singleton(conversationAnnotationParsers)); + } + + /** + * Constructor that uses a pre-built set with annotation parsers to retrieve the conversation meta-data. It is up to + * the caller to provide a sorted set of annotation parsers if the order of them is important. + * + * @param parsers The Set of annotation parsers used to retrieve conversation meta-data. + */ + public AnnotationConversationAttributeSource(Set parsers) { + Assert.notNull(parsers, "ConversationAnnotationParsers must not be null"); + conversationAnnotationParsers = parsers; + } + + /** + * Resolves the conversation meta-data by delegating to the ConversationAnnotationParser instances. This implementation + * returns the first ConversationAttribute instance that will be returned by a ConversationAnnotationParser. This + * method returns null if no ConversationAnnotationParser returns a non-null result. + * + * The implementation searches for the most specific method (e.g. if there is a interface method this methods searches + * for the implementation method) before calling the underlying ConversationAnnotationParser instances. If there is no + * Annotation available on the implementation method, this methods falls back to the interface method. + * + * @param method The method for which the ConversationAttribute should be returned. + * @param targetClass The target class where the implementation should look for. + */ + public ConversationAttribute getConversationAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + + for (ConversationAnnotationParser parser : conversationAnnotationParsers) { + ConversationAttribute attribute = parser.parseConversationAnnotation(specificMethod); + if (attribute != null) { + return attribute; + } + + if(method != specificMethod){ + attribute = parser.parseConversationAnnotation(method); + if(attribute != null){ + return attribute; + } + } + } + return null; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversation.java b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversation.java new file mode 100644 index 00000000000..d277b65cc1d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversation.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.conversation.Conversation; +import org.springframework.conversation.ConversationManager; +import org.springframework.conversation.ConversationType; +import org.springframework.conversation.interceptor.ConversationAttribute; + +/** + * This annotation can be placed on any method to start a new conversation. This has the same effect as invoking {@link + * org.springframework.conversation.ConversationManager#beginConversation(boolean, JoinMode)} using false + * for the temporary mode and the join mode as being specified within the annotation or {@link JoinMode#NEW} as the + * default.
The new conversation is always long running (not a temporary one) and is ended by either manually + * invoke {@link ConversationManager#endCurrentConversation(ConversationEndingType)}, invoking the {@link + * Conversation#end(ConversationEndingType)} method on the conversation itself or by placing the {@link EndConversation} + * annotation on a method.
The new conversation is created BEFORE the method itself is invoked as a before-advice. + * + * @author Micha Kiener + * @author Agim Emruli + * @since 3.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface BeginConversation { + /** + * The conversation type declares how to start a new conversation and how to handle an existing current one. + * See {@link ConversationType} for more information. + */ + ConversationType value() default ConversationType.NEW; + + /** The timeout for this conversation in seconds. */ + int timeout() default ConversationAttribute.DEFAULT_TIMEOUT; +} + diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversationAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversationAnnotationParser.java new file mode 100644 index 00000000000..a5c04b545c3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/BeginConversationAnnotationParser.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.annotation; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.conversation.interceptor.ConversationAttribute; +import org.springframework.conversation.interceptor.DefaultConversationAttribute; + +/** + * ConversationAnnotationParser for the BeginConversation annotation + * + * @author Agim Emruli + * @see BeginConversation + */ +class BeginConversationAnnotationParser implements ConversationAnnotationParser { + + public ConversationAttribute parseConversationAnnotation(AnnotatedElement annotatedElement) { + BeginConversation beginConversation = annotatedElement.getAnnotation(BeginConversation.class); + if (beginConversation != null) { + return new DefaultConversationAttribute(beginConversation.value(), beginConversation.timeout()); + } + + return null; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/annotation/ConversationAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/ConversationAnnotationParser.java new file mode 100644 index 00000000000..7e7d6749636 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/ConversationAnnotationParser.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.annotation; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.conversation.interceptor.ConversationAttribute; + +/** + * Parser interface that resolver the concrete conversation annotation from an AnnotatedElement. + * + * @author Agim Emruli + */ +interface ConversationAnnotationParser { + + /** + * This method returns the ConversationAttribute for a particular AnnotatedElement which can be a method or class at + * all. + */ + ConversationAttribute parseConversationAnnotation(AnnotatedElement annotatedElement); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversation.java b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversation.java new file mode 100644 index 00000000000..0e067a348a5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversation.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be placed on a method to end the current conversation. It + * has the same effect as a manual invocation of + * {@link org.springframework.conversation.ConversationManager#endCurrentConversation(ConversationEndingType)}.
+ * The conversation is ended AFTER the method was invoked as an after-advice. + * + * @author Micha Kiener + * @since 3.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface EndConversation { + + /** + * If root is true which is the default, using this annotation will end a current conversation + * completely including its path up to the top root conversation. If declared as false, it will + * only end the current conversation, making its parent as the new current conversation. + * If the current conversation is not a nested or isolated conversation, the root parameter has + * no impact. + */ + boolean root() default true; +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversationAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversationAnnotationParser.java new file mode 100644 index 00000000000..fb496ee89fc --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/annotation/EndConversationAnnotationParser.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.annotation; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.conversation.interceptor.ConversationAttribute; +import org.springframework.conversation.interceptor.DefaultConversationAttribute; + +/** + * ConversationAnnotationParser for the EndConversation annotation + * + * @author Agim Emruli + * @see EndConversation + */ +class EndConversationAnnotationParser implements ConversationAnnotationParser { + + public ConversationAttribute parseConversationAnnotation(AnnotatedElement annotatedElement) { + EndConversation endConversation = annotatedElement.getAnnotation(EndConversation.class); + if (endConversation != null) { + return new DefaultConversationAttribute(endConversation.root()); + } + return null; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/BeanFactoryConversationAttributeSourceAdvisor.java b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/BeanFactoryConversationAttributeSourceAdvisor.java new file mode 100644 index 00000000000..d702183ebee --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/BeanFactoryConversationAttributeSourceAdvisor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.interceptor; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; + +/** + * Advisor implementation that advises beans if they contain conversation meta-data. Uses a + * ConversationAttributeSourcePointcut to specify if the bean should be advised or not. + * + * @author Agim Emruli + */ +public class BeanFactoryConversationAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { + + private ConversationAttributeSource conversationAttributeSource; + + private Pointcut pointcut = new ConversationAttributeSourcePointcut() { + @Override + protected ConversationAttributeSource getConversationAttributeSource() { + return conversationAttributeSource; + } + }; + + /** + * Sets the ConversationAttributeSource instance that will be used to retrieve the ConversationDefinition meta-data. + * This instance will be used by the point-cut do specify if the target bean should be advised or not. + */ + public void setConversationAttributeSource(ConversationAttributeSource conversationAttributeSource) { + this.conversationAttributeSource = conversationAttributeSource; + } + + /** + * Returns the pointcut that will be used at runtime to test if the bean should be advised or not. + * + * @see org.springframework.conversation.interceptor.ConversationAttributeSourcePointcut + */ + public Pointcut getPointcut() { + return pointcut; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttribute.java b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttribute.java new file mode 100644 index 00000000000..a96d9bddc91 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttribute.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.interceptor; + +import org.springframework.conversation.ConversationType; + +/** + * ConversationDefinition Attributes that will be used by the AOP interceptor to start and end conversation before and + * after methods. This attributes are not in the conversation definition itself because these attributes are only + * relevant for a interceptor based approach to handle conversations. + * + * @author Agim Emruli + */ +public interface ConversationAttribute { + + /** + * The default timeout for a conversation, means there is no timeout defined for the conversation. It is up to the + * concrete conversation manager implementation to handle conversation without a timeout, like a indefinite + * conversation or some other system specific timeout + */ + int DEFAULT_TIMEOUT = -1; + + /** + * Defines if the a conversation should be started before the interceptor delegates to the target (like a method + * invocation). This can be a short-running conversation where the method is the whole life-cycle of a conversation or + * a long-running conversation where the conversation will be started but not stopped while calling the target. + * + * @return if the conversation should be started + */ + boolean shouldStartConversation(); + + /** + * Defines if the a conversation should be ended after the interceptor delegates to the target (like a method + * invocation). The stopped that should be stopped after the call to the target can be a short-running conversation + * that has been start before the method call or a long-running conversation that has been started on some other method + * call before in the life-cycle of the application. + * + * @return if the conversation should be ended + */ + boolean shouldEndConversation(); + + boolean shouldEndRoot(); + + /** + * Returns the type used to start a new conversation. See {@link org.springframework.conversation.ConversationType} + * for a more detailed description of the different types available.
+ * Default type is {@link org.springframework.conversation.ConversationType#NEW} which will create a new root + * conversation and will automatically end the current one, if not ended before. + * + * @return the conversation type to use for creating a new conversation + */ + ConversationType getConversationType(); + + /** + * Returns the timeout to be set within the newly created conversation, default is -1 which means to use + * the default timeout as being configured on the {@link org.springframework.conversation.ConversationManager}. + * A value of 0 means there is no timeout any other positive value is interpreted as a timeout in + * milliseconds. + * + * @return the timeout in milliseconds to be set on the new conversation + */ + int getTimeout(); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSource.java b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSource.java new file mode 100644 index 00000000000..6aff2c774a2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSource.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.interceptor; + +import java.lang.reflect.Method; + +/** + * Interface used by the ConversationInterceptor to retrieve the meta-data for the particular conversation. The + * meta-data can be provided by any implementation which is capable to return a ConversationAttribute instance based on + * a method and class. The implementation could be a annotation-based one or a XML-based implementation that retrieves + * the meta-data through a XML-configuration. + * + * @author Agim Emruli + */ +public interface ConversationAttributeSource { + + /** + * Resolves the ConversatioNAttribute for a particular method if available. This method must return null if there are + * no ConversationAttribute meta-data available for one particular method. It is up to the implementation to look for + * alternative sources like class-level annotations that applies to all methods inside a particular class. + * + * @param method The method for which the ConversationAttribute should be returned. + * @param targetClass The target class where the implementation should look for. + * @return the conversation attributes if available for the method, otherwise null. + */ + ConversationAttribute getConversationAttribute(Method method, Class targetClass); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSourcePointcut.java b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSourcePointcut.java new file mode 100644 index 00000000000..e2f45a615cd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationAttributeSourcePointcut.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.support.StaticMethodMatcherPointcut; + +/** + * Pointcut implementation that matches for methods where conversation meta-data is available for. This class is a base + * class for concrete Pointcut implementations that will provide the particular ConversationAttributeSource instance. + * + * @author Agim Emruli + */ +abstract class ConversationAttributeSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { + + public boolean matches(Method method, Class targetClass) { + ConversationAttributeSource attributeSource = getConversationAttributeSource(); + return (attributeSource != null && attributeSource.getConversationAttribute(method, targetClass) != null); + } + + /** + * @return - the ConversationAttributeSource instance that will be used to retrieve the conversation meta-data + */ + protected abstract ConversationAttributeSource getConversationAttributeSource(); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationInterceptor.java b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationInterceptor.java new file mode 100644 index 00000000000..4dedcb75f3f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/ConversationInterceptor.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.interceptor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.conversation.Conversation; +import org.springframework.conversation.ConversationManager; + +/** + * MethodInterceptor that manages conversations based on ConversationAttribute meta-data. + * + * @author Agim Emruli + */ +public class ConversationInterceptor implements MethodInterceptor { + + private ConversationManager conversationManager; + + private ConversationAttributeSource conversationAttributeSource; + + /** + * Sets the ConversationManager implementation that will be used to actually handle the conversations. + * + * @see org.springframework.conversation.manager.DefaultConversationManager + */ + public void setConversationManager(ConversationManager conversationManager) { + this.conversationManager = conversationManager; + } + + /** + * Sets the ConversationAttributeSource that will be used to retrieve the meta-data for one particular method at + * runtime. + * + * @see org.springframework.conversation.annotation.AnnotationConversationAttributeSource + */ + public void setConversationAttributeSource(ConversationAttributeSource conversationAttributeSource) { + this.conversationAttributeSource = conversationAttributeSource; + } + + /** + * Advice implementations that actually handles the conversations. This method retrieves and consults the + * ConversationAttribute at runtime and performs the particular actions before and after the target method call. + * + * @param invocation The MethodInvocation that represents the context object for this interceptor. + */ + public Object invoke(MethodInvocation invocation) throws Throwable { + + Class targetClass = (invocation.getThis() != null ? invocation.getThis().getClass() : null); + + ConversationAttribute conversationAttribute = + conversationAttributeSource.getConversationAttribute(invocation.getMethod(), targetClass); + + Object returnValue; + try { + + if (conversationAttribute != null && conversationAttribute.shouldStartConversation()) { + Conversation conversation = + conversationManager.beginConversation(conversationAttribute.getConversationType()); + if (conversationAttribute.getTimeout() != ConversationAttribute.DEFAULT_TIMEOUT) { + conversation.setTimeout(conversationAttribute.getTimeout()); + } + } + + returnValue = invocation.proceed(); + + if (conversationAttribute != null && conversationAttribute.shouldEndConversation()) { + conversationManager.endCurrentConversation(conversationAttribute.shouldEndRoot()); + } + + } + catch (Throwable th) { + if (conversationAttribute != null) { + conversationManager.endCurrentConversation(conversationAttribute.shouldEndRoot()); + } + throw th; + } + + return returnValue; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/DefaultConversationAttribute.java b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/DefaultConversationAttribute.java new file mode 100644 index 00000000000..6fd02910639 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/interceptor/DefaultConversationAttribute.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.interceptor; + +import org.springframework.conversation.ConversationType; + +/** + * Default implementation of the ConversationAttribute used by the conversation system. + * + * @author Agim Emruli + */ +public class DefaultConversationAttribute implements ConversationAttribute { + + private final boolean shouldStartConversation; + + private final boolean shouldEndConversation; + + private final ConversationType conversationType; + + private final boolean shouldEndRoot; + + private int timeout = DEFAULT_TIMEOUT; + + private DefaultConversationAttribute(boolean startConversation, + boolean endConversation, + ConversationType conversationType, + int timeout, boolean shouldEndRootConversation) { + shouldStartConversation = startConversation; + shouldEndConversation = endConversation; + this.conversationType = conversationType; + this.timeout = timeout; + this.shouldEndRoot = shouldEndRootConversation; + } + + public DefaultConversationAttribute(ConversationType conversationType, int timeout) { + this(true,false,conversationType,timeout, false); + } + + public DefaultConversationAttribute(boolean shouldEndRoot) { + this(false, true, null, DEFAULT_TIMEOUT, shouldEndRoot); + } + + public boolean shouldStartConversation() { + return shouldStartConversation; + } + + public boolean shouldEndConversation() { + return shouldEndConversation; + } + + public boolean shouldEndRoot() { + return shouldEndRoot; + } + + public ConversationType getConversationType() { + return conversationType; + } + + public int getTimeout() { + return timeout; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/AbstractConversationRepository.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/AbstractConversationRepository.java new file mode 100644 index 00000000000..2da188a178d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/AbstractConversationRepository.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +import org.springframework.conversation.Conversation; + +/** + * An abstract implementation for a conversation repository. Its implementation is based on the + * {@link org.springframework.conversation.manager.DefaultConversation} and manages its initial timeout and provides + * easy removal functionality. Internally, there is no explicit check for the conversation object implementing the + * {@link MutableConversation} interface, it is assumed to be implemented as the abstract repository also creates the + * conversation objects. + * + * @author Micha Kiener + * @since 3.1 + */ +public abstract class AbstractConversationRepository implements ConversationRepository { + + /** + * The default timeout in seconds for new conversations, can be setup using the Spring configuration of the + * repository. A value of 0 means there is no timeout. + */ + private int defaultTimeout = 0; + + /** + * Creates a new conversation object and initializes its timeout by using the default timeout being set on this + * repository. + */ + public MutableConversation createNewConversation() { + MutableConversation conversation = new DefaultConversation(); + conversation.setTimeout(getDefaultConversationTimeout()); + return conversation; + } + + /** + * Creates a new conversation, attaches it to the parent and initializes its timeout as being set on the parent. + */ + public MutableConversation createNewChildConversation(MutableConversation parentConversation, boolean isIsolated) { + MutableConversation childConversation = createNewConversation(); + parentConversation.addChildConversation(childConversation, isIsolated); + childConversation.setTimeout(parentConversation.getTimeout()); + return childConversation; + } + + /** + * Generic implementation of the remove method of a repository, handling the root flag automatically + * by invoking the {@link #removeConversation(org.springframework.conversation.Conversation)} by either passing in + * the root conversation or just the given conversation.
+ * Concrete repository implementations can overwrite the + * {@link #removeConversation(org.springframework.conversation.Conversation)} method to finally remove the + * conversation object or they might provide their own custom implementation for the remove operation by overwriting + * this method completely. + * + * @param id the id of the conversation to be removed + * @param root flag indicating whether the whole conversation hierarchy should be removed (true) or just + * the specified conversation + */ + public void removeConversation(String id, boolean root) { + MutableConversation conversation = getConversation(id); + if (conversation == null) { + return; + } + + if (root) { + removeConversation((MutableConversation)conversation.getRoot()); + } + else { + removeConversation(conversation); + } + } + + /** + * Internal, final method recursively invoking this method for all children of the given conversation. + * + * @param conversation the conversation to be removed, including its children, if any + */ + protected final void removeConversation(MutableConversation conversation) { + for (Conversation child : conversation.getChildren()) { + // remove the child from its parent and recursively invoke this method to remove the children of the + // current conversation + conversation.removeChildConversation((MutableConversation)child); + removeConversation((MutableConversation)child); + } + + // end the conversation (will internally clear the attributes, invoke destruction callbacks, if any, and + // invalidates the conversation + conversation.clear(); + conversation.invalidate(); + + // finally, remove the single object from the repository + removeSingleConversationObject((MutableConversation)conversation); + } + + /** + * Abstract removal method to be implemented by concrete repository implementations to remove the given, single + * conversation object. Any parent and child relations must not be handled within this method, just the removal of + * the given object. + * + * @param conversation the single conversation object to be removed from this repository + */ + protected abstract void removeSingleConversationObject(MutableConversation conversation); + + + public void setDefaultConversationTimeout(int defaultTimeout) { + this.defaultTimeout = defaultTimeout; + } + + public int getDefaultConversationTimeout() { + return defaultTimeout; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationRepository.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationRepository.java new file mode 100644 index 00000000000..93eb8ee22c9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationRepository.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +/** + * The conversation repository is responsible for creating new conversation objects, store them within its own storage + * and finally remove the after they have been ended.
+ * The repository might be transient (most likely in a web environment) but might support long running, persisted + * conversations as well.
+ * The repository is responsible for the timeout management of the conversation objects as well and might use an + * existing mechanism to do so (in a distributed, cached storage for instance).
+ * Depending on the underlying storage mechanism, the repository might support destruction callbacks within the + * conversation objects or not. If they are not supported, make sure an appropriate warning will be logged if + * registering a destruction callback. + * + * @author Micha Kiener + * @since 3.1 + */ +public interface ConversationRepository { + + /** + * Creates a new root conversation object and returns it. Be aware that this method does not store the conversation, + * this has to be done using the {@link #storeConversation(MutableConversation)} method. The id of the conversation + * will typically be set within the store method rather than the creation method. + * + * @return the newly created conversation object + */ + MutableConversation createNewConversation(); + + /** + * Creates a new child conversation and attaches it as a child to the given parent conversation. Like the + * {@link #createNewConversation()} method, this one does not store the new child conversation object, this has to + * be done using the {@link #storeConversation(MutableConversation)} method. The id of the new child conversation + * will typically be set within the store method rather than the creation method. + * + * @param parentConversation the parent conversation to create and attach a new child conversation to + * @param isIsolated true if the new child conversation has to be isolated from its parent state, + * false if it will inherit the state from the given parent + * @return the newly created child conversation, attached to the given parent conversation + */ + MutableConversation createNewChildConversation(MutableConversation parentConversation, boolean isIsolated); + + /** + * Returns the conversation with the given id which has to be registered before. If no such conversation is found, + * null is returned rather than throwing an exception. + * + * @param id the id to return the conversation from this store + * @return the conversation, if found, null otherwise + */ + MutableConversation getConversation(String id); + + /** + * Stores the given conversation object within this repository. Depending on its implementation, the storage might be + * transient or persistent, it might relay on other mechanisms like a session (in the area of web conversations for + * instance). After the conversation has been stored, its id must be set hence the id of the conversation will be + * available only after the store method has been invoked. + * + * @param conversation the conversation to be stored within the repository + */ + void storeConversation(MutableConversation conversation); + + /** + * Removes the conversation with the given id from this store. Depending on the root flag, the whole + * conversation hierarchy is removed or just the specified conversation. + * + * @param id the id of the conversation to be removed + * @param root flag indicating whether the whole conversation hierarchy should be removed (true) or just + * the specified conversation (false) + */ + void removeConversation(String id, boolean root); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationResolver.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationResolver.java new file mode 100644 index 00000000000..23d549f4be1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/ConversationResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +/** + * The current conversation resolver is another extension point for the conversation manager to easily change the + * default behavior of storing the currently used conversation id. + * + * In a web environment, the current conversation id would most likely be bound to the current window / tab and hence be + * based on the window management. This makes it possible to run different conversations per browser window and + * isolating them from each other by default. + * + * In a unit-test or batch environment the current conversation could be bound to the current thread to make + * conversations be available in a non-web environment as well. + * + * @author Micha Kiener + * @since 3.1 + */ +public interface ConversationResolver { + + /** + * Returns the id of the currently used conversation, if any, null otherwise. + * + * @return the id of the current conversation, if any, null otherwise + */ + String getCurrentConversationId(); + + /** + * Set the given conversation id to be the currently used one. Replaces the current one, if any, but is not removing + * the current conversation. + * + * @param conversationId the id of the conversation to be made the current one + */ + void setCurrentConversationId(String conversationId); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversation.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversation.java new file mode 100644 index 00000000000..42b1f7ae9a6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversation.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.config.DestructionAwareAttributeHolder; +import org.springframework.conversation.Conversation; + +/** + *

The default implementation of the {@link org.springframework.conversation.Conversation} and {@link + * MutableConversation} interfaces.
+ * This default implementation is also used within the {@link AbstractConversationRepository}. + *

+ *

+ * The implementation supports destruction callbacks for attributes. The conversation object is serializable as long as + * all of its attributes are serializable as well. + *

+ * + * @author Micha Kiener + * @since 3.1 + */ +public class DefaultConversation implements MutableConversation, Serializable { + /** Serializable identifier. */ + private static final long serialVersionUID = 1L; + + /** The conversation id which must be unique within the scope of its storage. The id is set by the repository. */ + private String id; + + /** The parent conversation, if this is a nested or isolated conversation. */ + private MutableConversation parent; + + /** The optional nested conversation(s), if this is a parent conversation. */ + private List children; + + /** The map with all the registered attributes and destruction callbacks. */ + private DestructionAwareAttributeHolder attributes = new DestructionAwareAttributeHolder(); + + /** + * If set to true, this conversation does not inherit the state of its parent but rather has its own, + * isolated state. This is set to true, if a new conversation with + * {@link org.springframework.conversation.ConversationType#ISOLATED} is created. + */ + private boolean isolated; + + /** The timeout in seconds or 0, if no timeout specified. */ + private int timeout; + + /** The system timestamp of the creation of this conversation. */ + private final long creationTime = System.currentTimeMillis(); + + /** The timestamp in milliseconds of the last access to this conversation. */ + private long lastAccess; + + /** Flag indicating whether this conversation has been invalidated already. */ + private boolean invalidated; + + + public DefaultConversation() { + touch(); + } + + /** + * Considers the internal attribute map as well as the map from the parent, if this is a nested conversation and only + * if it is not isolated. + */ + public Object getAttribute(String name) { + checkValidity(); + touch(); + + // first try to get the attribute from this conversation state + Object value = attributes.getAttribute(name); + if (value != null) { + return value; + } + + // the value was not found, try the parent conversation, if any and if + // not isolated + if (parent != null && !isolated) { + return parent.getAttribute(name); + } + + // this is the root conversation and the requested bean is not + // available, so return null instead + return null; + } + + public Object setAttribute(String name, Object value) { + checkValidity(); + touch(); + + return attributes.setAttribute(name, value); + } + + public Object removeAttribute(String name) { + checkValidity(); + touch(); + return attributes.removeAttribute(name); + } + + public void clear() { + attributes.clear(); + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Conversation getRoot() { + // check for having a parent to be returned as the root + if (parent != null) { + return parent.getRoot(); + } + + return this; + } + + public Conversation getParent() { + return parent; + } + + public List getChildren() { + if (children == null){ + return Collections.emptyList(); + } + + return children; + } + + protected void setParentConversation(MutableConversation parentConversation, boolean isIsolated) { + checkValidity(); + this.parent = parentConversation; + this.isolated = isIsolated; + } + + public void addChildConversation(MutableConversation conversation, boolean isIsolated) { + checkValidity(); + if (conversation instanceof DefaultConversation) { + // set this conversation as the parent within the given child conversation + ((DefaultConversation)conversation).setParentConversation(this, isIsolated); + } + + if (children == null) { + children = new ArrayList(); + } + + children.add(conversation); + } + + public void removeChildConversation(MutableConversation conversation) { + if (children != null) { + children.remove(conversation); + if (children.size() == 0) { + children = null; + } + } + } + + protected void removeFromParent() { + if (parent != null) { + parent.removeChildConversation(this); + } + + parent = null; + } + + public boolean isNested() { + return (parent != null); + } + + public boolean isParent() { + return (children != null && children.size() > 0); + } + + public boolean isIsolated() { + return isolated; + } + + /** + * Always returns the timeout value being set on the root as the root conversation is responsible for the timeout management. + */ + public int getTimeout() { + if (parent == null) { + return timeout; + } + else { + return getRoot().getTimeout(); + } + } + + /** + * The timeout will be set on the root only. + */ + public void setTimeout(int timeout) { + if (parent == null) { + this.timeout = timeout; + } + else { + getRoot().setTimeout(timeout); + } + } + + public long getCreationTime() { + return creationTime; + } + + public long getLastAccessedTime() { + return lastAccess; + } + + public void invalidate() { + invalidated = true; + clear(); + } + + protected void checkValidity() { + if (invalidated) { + throw new IllegalStateException("The conversation has been invalidated!"); + } + } + + /** + * Return true if the top root conversation has expired as the timeout is only tracked on the + * root conversation. + * + * @return true if the root of this conversation has been expired + */ + public boolean isExpired() { + if (parent != null) { + return parent.isExpired(); + } + + return (timeout != 0 && (lastAccess + timeout * 1000 < System.currentTimeMillis())); + } + + public void touch() { + lastAccess = System.currentTimeMillis(); + + // if this is a nested conversation, also touch its parent to make sure + // the parent is never timed out, if the + // current conversation is one of its nested conversations + if (parent != null) { + parent.touch(); + } + } + + public void registerDestructionCallback(String name, Runnable callback) { + attributes.registerDestructionCallback(name, callback); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversationManager.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversationManager.java new file mode 100644 index 00000000000..3cc0011a413 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/DefaultConversationManager.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +import org.springframework.conversation.ConversationManager; +import org.springframework.conversation.ConversationType; + +/** + * The default implementation for the {@link org.springframework.conversation.ConversationManager} interface. + * + * @author Micha Kiener + * @since 3.1 + */ +public class DefaultConversationManager implements ConversationManager { + + /** + * The conversation resolver used to resolve and expose the currently used conversation id. Do not use this attribute + * directly, always use the {@link #getConversationResolver()} method as the getter could have been injected. + */ + private ConversationResolver conversationResolver; + + /** + * If the store was injected, this attribute holds the reference to it. Never use the attribute directly, always use + * the {@link #getConversationRepository()} getter as it could have been injected. + */ + private ConversationRepository conversationRepository; + + + public MutableConversation getCurrentConversation() { + return getCurrentConversation(true); + } + + public MutableConversation getCurrentConversation(boolean createNewIfNotExisting) { + ConversationResolver resolver = getConversationResolver(); + + MutableConversation currentConversation = null; + String conversationId = resolver.getCurrentConversationId(); + if (conversationId != null) { + ConversationRepository repository = getConversationRepository(); + currentConversation = repository.getConversation(conversationId); + } + + if (currentConversation == null && createNewIfNotExisting) { + currentConversation = beginConversation(ConversationType.NEW); + } + + return currentConversation; + } + + /** + * The implementation uses the conversation repository to create a new root or a new child conversation depending on + * the conversation type specified. + * + * @param conversationType the type used to start a new conversation + * @return the newly created conversation + */ + public MutableConversation beginConversation(ConversationType conversationType) { + ConversationRepository repository = getConversationRepository(); + ConversationResolver resolver = getConversationResolver(); + + MutableConversation newConversation = null; + + switch (conversationType) { + case NEW: + // end the current conversation and create a new root one + endCurrentConversation(true); + newConversation = repository.createNewConversation(); + break; + + case NESTED: + case ISOLATED: + MutableConversation parentConversation = getCurrentConversation(false); + + // if a parent conversation is available, add the new conversation as its child conversation + if (parentConversation != null) { + newConversation = repository.createNewChildConversation(parentConversation, + conversationType == ConversationType.ISOLATED); + } + else { + // if no parent conversation found, create a new root one + newConversation = repository.createNewConversation(); + } + break; + } + + + // store the newly created conversation within its store and make it the current one through the resolver + repository.storeConversation(newConversation); + resolver.setCurrentConversationId(newConversation.getId()); + + return newConversation; + } + + /** + * The implementation only resolves the current conversation object using the repository, if only the given + * current conversation object and not the whole conversation hierarchy should be removed which can improve the + * removal from the underlying storage mechanism. + * + * @param root true to end the whole current conversation hierarchy or false to just + * remove the current conversation + */ + public void endCurrentConversation(boolean root) { + // remove the conversation from the repository and clear the current conversation id through the resolver + ConversationResolver resolver = getConversationResolver(); + ConversationRepository repository = getConversationRepository(); + + String currentConversationId = resolver.getCurrentConversationId(); + if (currentConversationId == null) { + return; + } + + // if only the current conversation has to be removed without the full conversation hierarchy, the + // current conversation must be set to the parent, if available + if (!root) { + MutableConversation currentConversation = repository.getConversation(currentConversationId); + if (currentConversation != null && currentConversation.getParent() != null) { + MutableConversation parentConversation = (MutableConversation)currentConversation.getParent(); + resolver.setCurrentConversationId(parentConversation.getId()); + } + } + + repository.removeConversation(currentConversationId, root); + } + + /** + * Returns the conversation resolver used to resolve the currently used conversation id. If the resolver itself has + * another scope than the manager, this method must be injected. + * + * @return the conversation resolver + */ + public ConversationResolver getConversationResolver() { + return conversationResolver; + } + + /** + * Inject the conversation resolver, if the method {@link #getConversationResolver()} is not injected and if the + * resolver has the same scope as the manager or even a more wider scope. + * + * @param conversationResolver the resolver to be injected + */ + public void setConversationResolver(ConversationResolver conversationResolver) { + this.conversationResolver = conversationResolver; + } + + /** + * Returns the repository where conversation objects are being registered. If the manager is in a wider scope than the + * repository, this method has to be injected. + * + * @return the conversation repository used to register conversation objects + */ + public ConversationRepository getConversationRepository() { + return conversationRepository; + } + + /** + * Inject the conversation repository to this manager which should only be done, if the method {@link + * #getConversationRepository()} is not injected and hence the repository has the same scope as the manager or wider. + * + * @param conversationRepository the repository to be injected + */ + public void setConversationRepository(ConversationRepository conversationRepository) { + this.conversationRepository = conversationRepository; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/LocalTransientConversationRepository.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/LocalTransientConversationRepository.java new file mode 100644 index 00000000000..da8e63b1345 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/LocalTransientConversationRepository.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.conversation.Conversation; + +/** + * A {@link ConversationRepository} storing the conversations within an internal map and hence assuming the conversation + * objects being transient. + * + * @author Micha Kiener + * @since 3.1 + */ +public class LocalTransientConversationRepository extends AbstractConversationRepository { + + /** The map for the conversation storage. */ + private final ConcurrentMap conversationMap = new ConcurrentHashMap(); + + /** Using an atomic integer, there is no need for synchronization while increasing the number. */ + private final AtomicInteger nextAvailableConversationId = new AtomicInteger(0); + + + public MutableConversation getConversation(String id) { + return conversationMap.get(id); + } + + public void storeConversation(MutableConversation conversation) { + conversation.setId(createNextConversationId()); + conversationMap.put(conversation.getId(), conversation); + } + + @Override + protected void removeSingleConversationObject(MutableConversation conversation) { + conversationMap.remove(conversation.getId()); + } + + public List getConversations() { + return new ArrayList(conversationMap.values()); + } + + public int size() { + return conversationMap.size(); + } + + protected String createNextConversationId(){ + return Integer.toString(nextAvailableConversationId.incrementAndGet()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/MutableConversation.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/MutableConversation.java new file mode 100644 index 00000000000..ad5a037dc47 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/MutableConversation.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +import org.springframework.conversation.Conversation; + +/** + *

+ * This interface extends the {@link Conversation} interface and is most likely used internally to modify the + * conversation object. Should never be used outside of the conversation management infrastructure. + *

+ * + * @author Micha Kiener + * @since 3.1 + */ +public interface MutableConversation extends Conversation { + + /** + * Set the id for this conversation which must be unique within the scope the conversation objects are being stored. + * The id of the conversation objects is usually managed by the {@link ConversationRepository}. + * + * @param id the id of the conversation + */ + void setId(String id); + + /** + * Method being invoked to add the given conversation as a child conversation to this parent conversation. If + * isIsolated is true, the state of the child conversation is isolated from its parent + * state, if it is set to false, the child conversation will inherit the state from its parent. + * + * @param conversation the conversation to be added as a child to this parent conversation + * @param isIsolated flag indicating whether this conversation should be isolated from the given parent conversation + */ + void addChildConversation(MutableConversation conversation, boolean isIsolated); + + /** + * Removes the given child conversation from this parent conversation. + * + * @param conversation the conversation to be removed from this one + */ + void removeChildConversation(MutableConversation conversation); + + /** + * Reset the last access timestamp using the current time in milliseconds from the system. This is usually done if a + * conversation is used behind a scope and beans are being accessed or added to it. + */ + void touch(); + + /** + * Clears the state of this conversation by removing all of its attributes. It will, however, not invalidate the + * conversation. All attributes having a destruction callback being registered will fire, if the underlying + * {@link ConversationRepository} supports destruction callbacks. + */ + void clear(); + + /** + * Returns true if this conversation has been expired. The expiration time (timeout) is only managed + * on the root conversation and is valid for the whole conversation hierarchy. + * + * @return true if this conversation has been expired, false otherwise + */ + boolean isExpired(); + + /** + * Invalidates this conversation object. An invalidated conversation will throw an {@link IllegalStateException}, + * if it is accessed or modified. + */ + void invalidate(); + + /** + * Registers the given callback to be invoked if the attribute having the specified name is being removed from this + * conversation. Supporting destruction callbacks is dependant of the underlying {@link ConversationRepository}, so + * this operation is optional and might not be supported. + * + * @param attributeName the name of the attribute to register the destruction callback for + * @param callback the callback to be invoked if the specified attribute is removed from this conversation + */ + void registerDestructionCallback(String attributeName, Runnable callback); +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/manager/ThreadLocalConversationResolver.java b/org.springframework.context/src/main/java/org/springframework/conversation/manager/ThreadLocalConversationResolver.java new file mode 100644 index 00000000000..c0063139e7c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/manager/ThreadLocalConversationResolver.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.manager; + +import org.springframework.core.NamedThreadLocal; + +/** + * An implementation of the {@link org.springframework.conversation.manager.ConversationResolver} where the currently + * used conversation id is bound to the current thread. + * If this implementation is used in a web environment, make sure the current conversation id is set and removed through + * a filter accordingly as the id is bound to the current thread using a thread local. + * + * @author Micha Kiener + * @since 3.1 + */ +public class ThreadLocalConversationResolver implements ConversationResolver{ + + /** The thread local attribute where the current conversation id is stored. */ + private final NamedThreadLocal currentConversationId = + new NamedThreadLocal("Current Conversation"); + + public String getCurrentConversationId() { + return currentConversationId.get(); + } + + public void setCurrentConversationId(String conversationId) { + currentConversationId.set(conversationId); + if (conversationId == null) { + currentConversationId.remove(); + } + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/conversation/scope/ConversationScope.java b/org.springframework.context/src/main/java/org/springframework/conversation/scope/ConversationScope.java new file mode 100644 index 00000000000..7d8cd820177 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/conversation/scope/ConversationScope.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.conversation.scope; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.conversation.Conversation; +import org.springframework.conversation.ConversationManager; +import org.springframework.conversation.manager.MutableConversation; + +/** + * The default implementation of a conversation scope most likely being exposed as "conversation" scope. + * It uses the {@link ConversationManager} to get access to the current conversation being used as the container for + * storing and retrieving attributes and beans. + * + * @author Micha Kiener + * @since 3.1 + */ +public class ConversationScope implements Scope { + + /** Holds the conversation manager reference, if statically injected. */ + private ConversationManager conversationManager; + + /** The name of the current conversation object, made available through {@link #resolveContextualObject(String)}. */ + public static final String CURRENT_CONVERSATION_ATTRIBUTE_NAME = "currentConversation"; + + + public Object get(String name, ObjectFactory objectFactory) { + Conversation conversation = getConversationManager().getCurrentConversation(true); + Object attribute = conversation.getAttribute(name); + if (attribute == null) { + attribute = objectFactory.getObject(); + conversation.setAttribute(name, attribute); + } + + return attribute; + } + + /** + * Will return null if there is no current conversation. It will not implicitly start a new one, if + * no current conversation object in place. + */ + public String getConversationId() { + Conversation conversation = getConversationManager().getCurrentConversation(false); + if (conversation != null) { + return conversation.getId(); + } + + return null; + } + + /** + * Registering a destruction callback is only possible, if supported by the underlying + * {@link org.springframework.conversation.manager.ConversationRepository}. + */ + public void registerDestructionCallback(String name, Runnable callback) { + Conversation conversation = getConversationManager().getCurrentConversation(false); + if (conversation instanceof MutableConversation) { + ((MutableConversation) conversation).registerDestructionCallback(name, callback); + } + } + + public Object remove(String name) { + Conversation conversation = getConversationManager().getCurrentConversation(false); + if (conversation != null) { + return conversation.removeAttribute(name); + } + + return null; + } + + /** + * Supports the following contextual objects: + *
    + *
  • "currentConversation", returns the current {@link org.springframework.conversation.Conversation}
  • + *
+ * + * @see org.springframework.beans.factory.config.Scope#resolveContextualObject(String) + */ + public Object resolveContextualObject(String key) { + if (CURRENT_CONVERSATION_ATTRIBUTE_NAME.equals(key)) { + return getConversationManager().getCurrentConversation(true); + } + + return null; + } + + public void setConversationManager(ConversationManager conversationManager) { + this.conversationManager = conversationManager; + } + + public ConversationManager getConversationManager() { + return conversationManager; + } +} diff --git a/org.springframework.context/template.mf b/org.springframework.context/template.mf index 57850ae472e..d33764be46b 100644 --- a/org.springframework.context/template.mf +++ b/org.springframework.context/template.mf @@ -35,6 +35,7 @@ Import-Template: org.springframework.aop.*;version=${spring.osgi.range};resolution:=optional, org.springframework.beans.*;version=${spring.osgi.range}, org.springframework.core.*;version=${spring.osgi.range}, + org.springframework.conversation.*;version=${spring.osgi.range}, org.springframework.expression.*;version=${spring.osgi.range};resolution:=optional, org.springframework.instrument.*;version="0";resolution:=optional, org.springframework.util.*;version=${spring.osgi.range}, diff --git a/org.springframework.web/src/main/java/org/springframework/web/conversation/SessionBasedConversationRepository.java b/org.springframework.web/src/main/java/org/springframework/web/conversation/SessionBasedConversationRepository.java new file mode 100644 index 00000000000..14c95da6a42 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/conversation/SessionBasedConversationRepository.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.web.conversation; + +import java.io.Serializable; + +import org.springframework.conversation.Conversation; +import org.springframework.conversation.manager.AbstractConversationRepository; +import org.springframework.conversation.manager.MutableConversation; +import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +/** + * A conversation store implementation ({@link org.springframework.conversation.manager.ConversationRepository}) based + * on a web environment where the conversations are most likely to be stored in the current session. + * + * @author Micha Kiener + * @since 3.1 + */ +public class SessionBasedConversationRepository extends AbstractConversationRepository { + + /** The name of the attribute for the conversation map within the session. */ + public static final String CONVERSATION_STORE_ATTR_NAME = SessionBasedConversationRepository.class.getName(); + + + public MutableConversation getConversation(String id) { + RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); + Object conversation = attributes + .getAttribute(getSessionAttributeNameForConversationId(id), RequestAttributes.SCOPE_GLOBAL_SESSION); + Assert.isInstanceOf(Conversation.class, conversation); + return (MutableConversation) conversation; + } + + public void storeConversation(MutableConversation conversation) { + conversation.setId(createNextConversationId()); + + RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); + String conversationSessionAttributeName = getSessionAttributeNameForConversationId(conversation.getId()); + + attributes.setAttribute(conversationSessionAttributeName, conversation, RequestAttributes.SCOPE_GLOBAL_SESSION); + attributes.registerDestructionCallback(conversationSessionAttributeName, + new ConversationDestructionCallback(conversation), RequestAttributes.SCOPE_GLOBAL_SESSION); + } + + @Override + protected void removeSingleConversationObject(MutableConversation conversation) { + RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); + attributes.removeAttribute(getSessionAttributeNameForConversationId(conversation.getId()), + RequestAttributes.SCOPE_GLOBAL_SESSION); + } + + protected String getSessionAttributeNameForConversationId(String conversationId) { + return CONVERSATION_STORE_ATTR_NAME + conversationId; + } + + protected String createNextConversationId() { + int nextAvailableConversationId = 1; + + RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); + synchronized (attributes.getSessionMutex()) { + String[] attributeNames = attributes.getAttributeNames(RequestAttributes.SCOPE_GLOBAL_SESSION); + for (String attributeName : attributeNames) { + if (attributeName.startsWith(CONVERSATION_STORE_ATTR_NAME)) { + + MutableConversation conversation = (MutableConversation) attributes + .getAttribute(attributeName, RequestAttributes.SCOPE_GLOBAL_SESSION); + if (conversation.isExpired()) { + conversation.clear(); + attributes.removeAttribute(attributeName, RequestAttributes.SCOPE_GLOBAL_SESSION); + } + + String conversationId = conversation.getId(); + int currentConversationId = Integer.parseInt(conversationId); + if (currentConversationId > nextAvailableConversationId) { + nextAvailableConversationId = currentConversationId; + } + } + } + } + return Integer.toString(nextAvailableConversationId); + } + + + private static final class ConversationDestructionCallback implements Runnable, Serializable { + + private final MutableConversation conversation; + + private ConversationDestructionCallback(MutableConversation conversation) { + this.conversation = conversation; + } + + public void run() { + conversation.clear(); + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/conversation/WebAwareConversationScope.java b/org.springframework.web/src/main/java/org/springframework/web/conversation/WebAwareConversationScope.java new file mode 100644 index 00000000000..d5d777725b3 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/conversation/WebAwareConversationScope.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.web.conversation; + +import org.springframework.conversation.scope.ConversationScope; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +/** + * The extension of the default conversation scope ( {@link ConversationScope}) by supporting contextual web + * objects returned by overwriting {@link #resolveContextualObject(String)}. + * + * @author Micha Kiener + * @since 3.1 + */ +public class WebAwareConversationScope extends ConversationScope { + + /** @see org.springframework.conversation.scope.ConversationScope#resolveContextualObject(String) */ + @Override + public Object resolveContextualObject(String key) { + // invoke super to support the conversation context objects + Object object = super.resolveContextualObject(key); + if (object != null) { + return object; + } + + // support web context objects + RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); + return attributes.resolveReference(key); + } +} diff --git a/org.springframework.web/template.mf b/org.springframework.web/template.mf index a8bc0c66a41..ad4ba30b7cc 100644 --- a/org.springframework.web/template.mf +++ b/org.springframework.web/template.mf @@ -27,6 +27,8 @@ Import-Template: org.springframework.beans.*;version=${spring.osgi.range}, org.springframework.context.*;version=${spring.osgi.range}, org.springframework.core.*;version=${spring.osgi.range}, + org.springframework.conversation.*;version=${spring.osgi.range}, + org.springframework.web.conversation.*;version=${spring.osgi.range}, org.springframework.jndi.*;version=${spring.osgi.range};resolution:=optional, org.springframework.oxm.*;version=${spring.osgi.range};resolution:=optional, org.springframework.remoting.*;version=${spring.osgi.range};resolution:=optional,