SPR-6416, initial commit for the conversation management
This commit is contained in:
parent
0c736776da
commit
f812cd748e
|
|
@ -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<String, Object> attributes = new ConcurrentHashMap<String, Object>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional map having any destruction callbacks registered using the
|
||||||
|
* name of the bean as the key.
|
||||||
|
*/
|
||||||
|
private Map<String, Runnable> 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<String, Object> getAttributeMap() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the attribute having the specified name, if available,
|
||||||
|
* <code>null</code> otherwise.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* the name of the attribute to be returned
|
||||||
|
* @return the attribute value or <code>null</code> 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,
|
||||||
|
* <code>null</code> otherwise
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Object setAttribute(String name, Object value) {
|
||||||
|
return attributes.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the object with the given <code>name</code> from the underlying
|
||||||
|
* scope.
|
||||||
|
* <p>
|
||||||
|
* Returns <code>null</code> if no object was found; otherwise returns the
|
||||||
|
* removed <code>Object</code>.
|
||||||
|
* <p>
|
||||||
|
* Note that an implementation should also remove a registered destruction
|
||||||
|
* callback for the specified object, if any. It does, however, <i>not</i>
|
||||||
|
* need to <i>execute</i> a registered destruction callback in this case,
|
||||||
|
* since the object will be destroyed by the caller (if appropriate).
|
||||||
|
* <p>
|
||||||
|
* <b>Note: This is an optional operation.</b> 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 <code>null</code> 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).
|
||||||
|
* <p>
|
||||||
|
* <b>Note: This is an optional operation.</b> 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 <i>must be ignored and a
|
||||||
|
* corresponding warning should be logged</i>.
|
||||||
|
* <p>
|
||||||
|
* 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<String, Runnable>();
|
||||||
|
}
|
||||||
|
|
||||||
|
registeredDestructionCallbacks.put(name, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the destruction callback, if any registered for the attribute
|
||||||
|
* with the given name or <code>null</code> if no such callback was
|
||||||
|
* registered.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* the name of the registered callback requested
|
||||||
|
* @param remove
|
||||||
|
* <code>true</code>, if the callback should be removed after
|
||||||
|
* this call, <code>false</code>, if it stays
|
||||||
|
* @return the callback, if found, <code>null</code> otherwise
|
||||||
|
*/
|
||||||
|
public Runnable getDestructionCallback(String name, boolean remove) {
|
||||||
|
if (registeredDestructionCallbacks == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove) {
|
||||||
|
return registeredDestructionCallbacks.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return registeredDestructionCallbacks.get(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}.<br/>
|
||||||
|
* 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, <code>null</code> otherwise.<br/> 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, <code>null</code> otherwise
|
||||||
|
*/
|
||||||
|
Object setAttribute(String name, Object value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value attached to the given name, if any previously registered, <code>null</code> otherwise.<br/>
|
||||||
|
* 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, <code>null</code> otherwise
|
||||||
|
*/
|
||||||
|
Object getAttribute(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the value in the current conversation having the given name and returns it, if found and removed,
|
||||||
|
* <code>null</code> otherwise.<br/> 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, <code>null</code> 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 <code>null</code>.
|
||||||
|
*
|
||||||
|
* @return the root conversation (top level conversation)
|
||||||
|
*/
|
||||||
|
Conversation getRoot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parent conversation, if this is a nested conversation, <code>null</code> otherwise.
|
||||||
|
*
|
||||||
|
* @return the parent conversation, if any, <code>null</code> otherwise
|
||||||
|
*/
|
||||||
|
Conversation getParent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of child conversations, if any, an empty list otherwise, must never return <code>null</code>.
|
||||||
|
*
|
||||||
|
* @return a list of child conversations (may be empty, never <code>null</code>)
|
||||||
|
*/
|
||||||
|
List<? extends Conversation> getChildren();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code>, if this is a nested conversation and hence {@link #getParent()} will returns a non-null
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* @return <code>true</code>, if this is a nested conversation, <code>false</code> otherwise
|
||||||
|
*/
|
||||||
|
boolean isNested();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code>, 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 <code>true</code>, 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 <code>0</code> 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, <code>0</code> otherwise
|
||||||
|
*/
|
||||||
|
int getTimeout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the timeout of this conversation hierarchy in seconds. A value of <code>0</code> 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, <code>0</code> for no timeout
|
||||||
|
*/
|
||||||
|
void setTimeout(int timeout);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* A conversation manager might be used manually in order to start and end conversations manually.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 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.<br/><br/>
|
||||||
|
*
|
||||||
|
* 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.<br/>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Extending the conversation management</b><br/>
|
||||||
|
* 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.<br/>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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 <code>null</code>, 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 <code>createIfNotExisting</code> is specified as <code>true</code>.
|
||||||
|
*
|
||||||
|
* @param createNewIfNotExisting <code>true</code>, if a new conversation should be created, if there is currently
|
||||||
|
* no active conversation in place, <code>false</code> to return <code>null</code>, if no current conversation active
|
||||||
|
* @return the current conversation or <code>null</code>, if no current conversation available and
|
||||||
|
* <code>createIfNotExisting</code> is set as <code>false</code>
|
||||||
|
*/
|
||||||
|
Conversation getCurrentConversation(boolean createNewIfNotExisting);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new conversation according the given <code>conversationType</code> and makes it the current active
|
||||||
|
* conversation. See {@link ConversationType} for more detailed information about the different conversation
|
||||||
|
* creation types available.<br/>
|
||||||
|
* 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 <code>root</code> is <code>true</code>, the whole conversation
|
||||||
|
* hierarchy is ended and there will no current conversation be active afterwards. If <code>root</code> is
|
||||||
|
* <code>false</code>, the current conversation is ended and if it is a nested one, its parent is made the
|
||||||
|
* current conversation.
|
||||||
|
*
|
||||||
|
* @param root <code>true</code> to end the whole current conversation hierarchy or <code>false</code> to just
|
||||||
|
* end the current conversation
|
||||||
|
*/
|
||||||
|
void endCurrentConversation(boolean root);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<ConversationAnnotationParser> 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<ConversationAnnotationParser> defaultParsers = new LinkedHashSet<ConversationAnnotationParser>();
|
||||||
|
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<ConversationAnnotationParser> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <code>false</code>
|
||||||
|
* for the temporary mode and the join mode as being specified within the annotation or {@link JoinMode#NEW} as the
|
||||||
|
* default.<br/> 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.<br/> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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)}.<br/>
|
||||||
|
* 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 <code>true</code> 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 <code>false</code>, 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 <code>root</code> parameter has
|
||||||
|
* no impact.
|
||||||
|
*/
|
||||||
|
boolean root() default true;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.<br/>
|
||||||
|
* 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 <code>-1</code> which means to use
|
||||||
|
* the default timeout as being configured on the {@link org.springframework.conversation.ConversationManager}.
|
||||||
|
* A value of <code>0</code> 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();
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <code>0</code> 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 <code>root</code> flag automatically
|
||||||
|
* by invoking the {@link #removeConversation(org.springframework.conversation.Conversation)} by either passing in
|
||||||
|
* the root conversation or just the given conversation.<br/>
|
||||||
|
* 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 (<code>true</code>) 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.<br/>
|
||||||
|
* The repository might be transient (most likely in a web environment) but might support long running, persisted
|
||||||
|
* conversations as well.<br/>
|
||||||
|
* 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).<br/>
|
||||||
|
* 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 <code>true</code> if the new child conversation has to be isolated from its parent state,
|
||||||
|
* <code>false</code> 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,
|
||||||
|
* <code>null</code> is returned rather than throwing an exception.
|
||||||
|
*
|
||||||
|
* @param id the id to return the conversation from this store
|
||||||
|
* @return the conversation, if found, <code>null</code> 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 <code>root</code> 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 (<code>true</code>) or just
|
||||||
|
* the specified conversation (<code>false</code>)
|
||||||
|
*/
|
||||||
|
void removeConversation(String id, boolean root);
|
||||||
|
}
|
||||||
|
|
@ -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, <code>null</code> otherwise.
|
||||||
|
*
|
||||||
|
* @return the id of the current conversation, if any, <code>null</code> 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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p> The default implementation of the {@link org.springframework.conversation.Conversation} and {@link
|
||||||
|
* MutableConversation} interfaces.<br/>
|
||||||
|
* This default implementation is also used within the {@link AbstractConversationRepository}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The implementation supports destruction callbacks for attributes. The conversation object is serializable as long as
|
||||||
|
* all of its attributes are serializable as well.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<MutableConversation> children;
|
||||||
|
|
||||||
|
/** The map with all the registered attributes and destruction callbacks. */
|
||||||
|
private DestructionAwareAttributeHolder attributes = new DestructionAwareAttributeHolder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to <code>true</code>, this conversation does not inherit the state of its parent but rather has its own,
|
||||||
|
* isolated state. This is set to <code>true</code>, if a new conversation with
|
||||||
|
* {@link org.springframework.conversation.ConversationType#ISOLATED} is created.
|
||||||
|
*/
|
||||||
|
private boolean isolated;
|
||||||
|
|
||||||
|
/** The timeout in seconds or <code>0</code>, 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<? extends Conversation> 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<MutableConversation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <code>true</code> if the top root conversation has expired as the timeout is only tracked on the
|
||||||
|
* root conversation.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <code>true</code> to end the whole current conversation hierarchy or <code>false</code> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, MutableConversation> conversationMap = new ConcurrentHashMap<String, MutableConversation>();
|
||||||
|
|
||||||
|
/** 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<Conversation> getConversations() {
|
||||||
|
return new ArrayList<Conversation>(conversationMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return conversationMap.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createNextConversationId(){
|
||||||
|
return Integer.toString(nextAvailableConversationId.incrementAndGet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <code>isIsolated</code> is <code>true</code>, the state of the child conversation is isolated from its parent
|
||||||
|
* state, if it is set to <code>false</code>, 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 <code>true</code> 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 <code>true</code> if this conversation has been expired, <code>false</code> 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);
|
||||||
|
}
|
||||||
|
|
@ -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<String> currentConversationId =
|
||||||
|
new NamedThreadLocal<String>("Current Conversation");
|
||||||
|
|
||||||
|
public String getCurrentConversationId() {
|
||||||
|
return currentConversationId.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentConversationId(String conversationId) {
|
||||||
|
currentConversationId.set(conversationId);
|
||||||
|
if (conversationId == null) {
|
||||||
|
currentConversationId.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <code>"conversation"</code> 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 <code>null</code> 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:
|
||||||
|
* <ul>
|
||||||
|
* <li><code>"currentConversation"</code>, returns the current {@link org.springframework.conversation.Conversation}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ Import-Template:
|
||||||
org.springframework.aop.*;version=${spring.osgi.range};resolution:=optional,
|
org.springframework.aop.*;version=${spring.osgi.range};resolution:=optional,
|
||||||
org.springframework.beans.*;version=${spring.osgi.range},
|
org.springframework.beans.*;version=${spring.osgi.range},
|
||||||
org.springframework.core.*;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.expression.*;version=${spring.osgi.range};resolution:=optional,
|
||||||
org.springframework.instrument.*;version="0";resolution:=optional,
|
org.springframework.instrument.*;version="0";resolution:=optional,
|
||||||
org.springframework.util.*;version=${spring.osgi.range},
|
org.springframework.util.*;version=${spring.osgi.range},
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,8 @@ Import-Template:
|
||||||
org.springframework.beans.*;version=${spring.osgi.range},
|
org.springframework.beans.*;version=${spring.osgi.range},
|
||||||
org.springframework.context.*;version=${spring.osgi.range},
|
org.springframework.context.*;version=${spring.osgi.range},
|
||||||
org.springframework.core.*;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.jndi.*;version=${spring.osgi.range};resolution:=optional,
|
||||||
org.springframework.oxm.*;version=${spring.osgi.range};resolution:=optional,
|
org.springframework.oxm.*;version=${spring.osgi.range};resolution:=optional,
|
||||||
org.springframework.remoting.*;version=${spring.osgi.range};resolution:=optional,
|
org.springframework.remoting.*;version=${spring.osgi.range};resolution:=optional,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue