提交 f812cd74 编写于 作者: M Micha Kiener

SPR-6416, initial commit for the conversation management

上级 0c736776
/*
* 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);
}
}
/*
* 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);
}
/*
* 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);
}
/*
* 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
}
/*
* 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;
}
}
/*
* 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;
}
/*
* 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;
}
}
/*
* 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);
}
/*
* 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;
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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();
}
/*
* 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);
}
/*
* 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();
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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);
}
/*
* 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);
}
/*
* 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);
}
}
/*
* 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;
}
}
/*
* 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());
}
}
/*
* 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);
}
/*
* 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();
}
}
}
/*
* 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.beans.*;version=${spring.osgi.range},
org.springframework.core.*;version=${spring.osgi.range},
org.springframework.conversation.*;version=${spring.osgi.range},
org.springframework.expression.*;version=${spring.osgi.range};resolution:=optional,
org.springframework.instrument.*;version="0";resolution:=optional,
org.springframework.util.*;version=${spring.osgi.range},
......
/*
* 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();
}
}
}
/*
* 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.context.*;version=${spring.osgi.range},
org.springframework.core.*;version=${spring.osgi.range},
org.springframework.conversation.*;version=${spring.osgi.range},
org.springframework.web.conversation.*;version=${spring.osgi.range},
org.springframework.jndi.*;version=${spring.osgi.range};resolution:=optional,
org.springframework.oxm.*;version=${spring.osgi.range};resolution:=optional,
org.springframework.remoting.*;version=${spring.osgi.range};resolution:=optional,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册