From 0ac5785ec5c92ef2f464316d6bbe5d92a72d82bd Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Wed, 23 Dec 2015 22:20:57 +0000 Subject: [PATCH] Overhaul annotation and listener processing - This is state 1 for these changes. - This is a stage one of fixes for #126 and #138, thus not giving full support not complete features. - Add new annotations in a side of OnTransition reflecting stuff from a StateMachineListener. - Prepare and modify call chains so that we have preliminary support(not feature ready) for these so that existing tests pass. - Add new helper to handle some annotation and spel call caching. - Enhance StateContext to better suit these needs. --- .../statemachine/StateContext.java | 46 +- .../annotation/ExtendedStateVariable.java | 37 ++ .../annotation/OnEventNotAccepted.java | 54 +++ .../annotation/OnExtendedStateChanged.java | 49 ++ .../annotation/OnStateChanged.java | 62 +++ .../statemachine/annotation/OnStateEntry.java | 62 +++ .../statemachine/annotation/OnStateExit.java | 62 +++ .../annotation/OnStateMachineError.java | 46 ++ .../annotation/OnStateMachineStart.java | 44 ++ .../annotation/OnStateMachineStop.java | 39 ++ .../statemachine/annotation/OnTransition.java | 26 ++ .../annotation/OnTransitionEnd.java | 61 +++ .../annotation/OnTransitionStart.java | 61 +++ ...chineActivatorAnnotationPostProcessor.java | 13 +- .../StateMachineAnnotationPostProcessor.java | 106 +++-- .../processor/StateMachineHandler.java | 50 ++- .../StateMachineHandlerCallHelper.java | 356 +++++++++++++++ .../StateMachineMethodInvokerHelper.java | 28 +- .../StateMachineOnTransitionHandler.java | 69 --- .../support/AbstractStateMachine.java | 167 ++----- .../support/DefaultStateContext.java | 35 +- .../support/StateMachineObjectSupport.java | 56 ++- .../statemachine/EnumStateMachineTests.java | 8 + .../statemachine/RegionMachineTests.java | 8 + .../statemachine/SubStateMachineTests.java | 13 + .../annotation/MethodAnnotationTests.java | 425 ++++++++++++++++-- .../processor/AnnotatedMethodTests.java | 1 - .../statemachine/state/RegionStateTests.java | 4 + .../state/SubmachineStateTests.java | 4 + 29 files changed, 1666 insertions(+), 326 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/ExtendedStateVariable.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnEventNotAccepted.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnExtendedStateChanged.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateChanged.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateEntry.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateExit.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineError.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStart.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStop.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionEnd.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionStart.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java delete mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineOnTransitionHandler.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java index a49fb426..318dd8e1 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java @@ -15,29 +15,44 @@ */ package org.springframework.statemachine; +import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; /** - * {@code StateContext} is representing a current context used in + * {@code StateContext} is representing of a current context used in + * various stages in a state machine execution. These include for example * {@link Transition}s, {@link Action}s and {@link Guard}s order to get access * to event headers and {@link ExtendedState}. + *

+ * Context really is not a current state of a state machine but + * more like a snapshot of where state machine is when this context + * is passed to various methods. * * @author Janne Valkealahti * */ public interface StateContext { + /** + * Gets the message associated with a context. Message may be null if transition + * is not triggered by a signal. + * + * @return the message + */ + Message getMessage(); + /** * Gets the event associated with a context. Event may be null if transition * is not triggered by a signal. - * + * * @return the event */ E getEvent(); - + /** * Gets the event message headers. * @@ -75,4 +90,29 @@ public interface StateContext { */ StateMachine getStateMachine(); + /** + * Gets the source state of this context. Generally source + * is where a state machine is coming from which may be different + * than what the transition source is. + * + * @return the source state + */ + State getSource(); + + /** + * Gets the tarter state of this context. Generally target + * is where a state machine going to which may be different + * than what the transition target is. + * + * @return the target state + */ + State getTarget(); + + /** + * Gets the exception associated with a context. + * + * @return the exception + */ + Exception getException(); + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/ExtendedStateVariable.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/ExtendedStateVariable.java new file mode 100644 index 00000000..58c7289d --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/ExtendedStateVariable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 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.statemachine.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.core.annotation.AliasFor; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ExtendedStateVariable { + + @AliasFor("key") + String value() default ""; + + @AliasFor("value") + String key() default ""; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnEventNotAccepted.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnEventNotAccepted.java new file mode 100644 index 00000000..849908db --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnEventNotAccepted.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; + +/** + * Indicates that a method is candidate to be called when event + * is not accepted by a state machine. + *

+ * A method annotated with @OnEventNotAccepted may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnEventNotAccepted { + + /** + * The events. + * + * @return the events + */ + String[] event() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnExtendedStateChanged.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnExtendedStateChanged.java new file mode 100644 index 00000000..5a3d5928 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnExtendedStateChanged.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.statemachine.ExtendedState; + +/** + * Indicates that a method is candidate to be called when {@link ExtendedState} + * is changed. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnExtendedStateChanged { + + /** + * The extended state variable keys. + * + * @return The extended state variable keys + */ + String[] key() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateChanged.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateChanged.java new file mode 100644 index 00000000..43e88bb7 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateChanged.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.state.State; + +/** + * Indicates that a method is candidate to be called when {@link State} + * is changed. + *

+ * A method annotated with @OnStateChanged may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnStateChanged { + + /** + * The source states. + * + * @return the source states. + */ + String[] source() default {}; + + /** + * The target states. + * + * @return the target states. + */ + String[] target() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateEntry.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateEntry.java new file mode 100644 index 00000000..2482db09 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateEntry.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.state.State; + +/** + * Indicates that a method is candidate to be called when {@link State} + * is entered. + *

+ * A method annotated with @OnStateChanged may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnStateEntry { + + /** + * The source states. + * + * @return the source states. + */ + String[] source() default {}; + + /** + * The target states. + * + * @return the target states. + */ + String[] target() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateExit.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateExit.java new file mode 100644 index 00000000..3add82b1 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateExit.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.state.State; + +/** + * Indicates that a method is candidate to be called when {@link State} + * is exited. + *

+ * A method annotated with @OnStateChanged may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnStateExit { + + /** + * The source states. + * + * @return the source states. + */ + String[] source() default {}; + + /** + * The target states. + * + * @return the target states. + */ + String[] target() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineError.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineError.java new file mode 100644 index 00000000..040eee37 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineError.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; + +/** + * Indicates that a method is candidate to be called when state machine + * has been entered in error it cannot recover. + *

+ * A method annotated with @OnStateMachineError may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnStateMachineError { +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStart.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStart.java new file mode 100644 index 00000000..e0a41488 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStart.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.statemachine.StateMachine; + +/** + * Indicates that a method is a candidate to be called when state machine + * is started. + *

+ * A method annotated with @OnStateMachineStart may accept a parameter of type + * {@link StateMachine}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnStateMachineStart { +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStop.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStop.java new file mode 100644 index 00000000..1c92ad40 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnStateMachineStop.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a method is a candidate to be called when state machine + * is stopped. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnStateMachineStop { +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransition.java index e9badec1..3b60de59 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransition.java @@ -21,15 +21,41 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Map; +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.transition.Transition; + +/** + * Indicates that a method is candidate to be called with a {@link Transition}. + *

+ * A method annotated with @OnTransition may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface OnTransition { + /** + * The source states. + * + * @return the source states. + */ String[] source() default {}; + /** + * The target states. + * + * @return the target states. + */ String[] target() default {}; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionEnd.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionEnd.java new file mode 100644 index 00000000..6bbb2992 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionEnd.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.transition.Transition; + +/** + * Indicates that a method is candidate to be called with a {@link Transition}. + *

+ * A method annotated with @OnTransitionEnd may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnTransitionEnd { + + /** + * The source states. + * + * @return the source states. + */ + String[] source() default {}; + + /** + * The target states. + * + * @return the target states. + */ + String[] target() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionStart.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionStart.java new file mode 100644 index 00000000..97951e3d --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/OnTransitionStart.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 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.statemachine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.transition.Transition; + +/** + * Indicates that a method is candidate to be called with a {@link Transition}. + *

+ * A method annotated with @OnTransitionStart may accept a parameter of type + * {@link ExtendedState} or {@link Map} if map argument is itself is annotated + * with {@link EventHeaders}. + *

+ * Return value can be anything and is effectively discarded. + * + * @author Janne Valkealahti + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface OnTransitionStart { + + /** + * The source states. + * + * @return the source states. + */ + String[] source() default {}; + + /** + * The target states. + * + * @return the target states. + */ + String[] target() default {}; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineActivatorAnnotationPostProcessor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineActivatorAnnotationPostProcessor.java index b6018fc3..7792fcac 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineActivatorAnnotationPostProcessor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineActivatorAnnotationPostProcessor.java @@ -23,15 +23,8 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.core.annotation.OrderUtils; -import org.springframework.statemachine.annotation.OnTransition; -/** - * Post-processor for Methods annotated with {@link OnTransition}. - * - * @author Janne Valkealahti - * - */ -public class StateMachineActivatorAnnotationPostProcessor implements MethodAnnotationPostProcessor{ +public class StateMachineActivatorAnnotationPostProcessor implements MethodAnnotationPostProcessor{ protected final BeanFactory beanFactory; @@ -40,8 +33,8 @@ public class StateMachineActivatorAnnotationPostProcessor implements MethodAnnot } @Override - public Object postProcess(Class beanClass, Object bean, String beanName, Method method, OnTransition metaAnnotation, Annotation annotation) { - StateMachineHandler handler = new StateMachineOnTransitionHandler(beanClass, bean, method, metaAnnotation, annotation); + public Object postProcess(Class beanClass, Object bean, String beanName, Method method, T metaAnnotation, Annotation annotation) { + StateMachineHandler handler = new StateMachineHandler(beanClass, bean, method, metaAnnotation, annotation); Integer order = findOrder(bean, method); if (order != null) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java index 60993361..e46038d7 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java @@ -41,7 +41,17 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.Lifecycle; import org.springframework.context.SmartLifecycle; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.statemachine.annotation.OnEventNotAccepted; +import org.springframework.statemachine.annotation.OnExtendedStateChanged; +import org.springframework.statemachine.annotation.OnStateChanged; +import org.springframework.statemachine.annotation.OnStateEntry; +import org.springframework.statemachine.annotation.OnStateExit; +import org.springframework.statemachine.annotation.OnStateMachineError; +import org.springframework.statemachine.annotation.OnStateMachineStart; +import org.springframework.statemachine.annotation.OnStateMachineStop; import org.springframework.statemachine.annotation.OnTransition; +import org.springframework.statemachine.annotation.OnTransitionEnd; +import org.springframework.statemachine.annotation.OnTransitionStart; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -93,7 +103,28 @@ public class StateMachineAnnotationPostProcessor implements BeanPostProcessor, B @Override public void afterPropertiesSet() { Assert.notNull(beanFactory, "BeanFactory must not be null"); - postProcessors.put(OnTransition.class, new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnTransition.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnTransitionStart.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnTransitionEnd.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnStateChanged.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnStateEntry.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnStateExit.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnStateMachineStart.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnStateMachineStop.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnEventNotAccepted.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnStateMachineError.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); + postProcessors.put(OnExtendedStateChanged.class, + new StateMachineActivatorAnnotationPostProcessor(beanFactory)); } @Override @@ -115,51 +146,42 @@ public class StateMachineAnnotationPostProcessor implements BeanPostProcessor, B @SuppressWarnings({ "unchecked", "rawtypes" }) public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - Annotation[] annotations = AnnotationUtils.getAnnotations(method); - - for (Annotation annotation : annotations) { - - Annotation metaAnnotation = null; - for (Class ppa : postProcessors.keySet()) { - Annotation a = AnnotationUtils.getAnnotation(annotation,ppa); - if (a != null && annotation.getClass().equals(a.getClass())) { - metaAnnotation = a; - } else { - metaAnnotation = a; - } + for (Class ppa : postProcessors.keySet()) { + Annotation metaAnnotation = AnnotationUtils.findAnnotation(method, ppa); + if (metaAnnotation == null) { + continue; } + for (Annotation a : AnnotationUtils.getAnnotations(method)) { + MethodAnnotationPostProcessor postProcessor = metaAnnotation != null ? postProcessors + .get(metaAnnotation.annotationType()) : null; + if (postProcessor != null && shouldCreateHandler(a)) { + Object result = postProcessor.postProcess(beanClass, bean, beanName, method, metaAnnotation, a); + if (result != null && result instanceof StateMachineHandler) { + String endpointBeanName = generateBeanName(beanName, method, a.annotationType()); - MethodAnnotationPostProcessor postProcessor = metaAnnotation != null ? postProcessors - .get(metaAnnotation.annotationType()) : null; - - if (postProcessor != null && shouldCreateHandler(annotation)) { - Object result = postProcessor.postProcess(beanClass, bean, beanName, method, metaAnnotation, annotation); - - if (result != null && result instanceof StateMachineHandler) { - String endpointBeanName = generateBeanName(beanName, method, annotation.annotationType()); - - if (result instanceof BeanNameAware) { - ((BeanNameAware) result).setBeanName(endpointBeanName); - } - beanFactory.registerSingleton(endpointBeanName, result); - if (result instanceof BeanFactoryAware) { - ((BeanFactoryAware) result).setBeanFactory(beanFactory); - } - if (result instanceof InitializingBean) { - try { - ((InitializingBean) result).afterPropertiesSet(); - } catch (Exception e) { - throw new BeanInitializationException("failed to initialize annotated component", e); + if (result instanceof BeanNameAware) { + ((BeanNameAware) result).setBeanName(endpointBeanName); } - } - if (result instanceof Lifecycle) { - lifecycles.add((Lifecycle) result); - if (result instanceof SmartLifecycle && ((SmartLifecycle) result).isAutoStartup()) { - ((SmartLifecycle) result).start(); + beanFactory.registerSingleton(endpointBeanName, result); + if (result instanceof BeanFactoryAware) { + ((BeanFactoryAware) result).setBeanFactory(beanFactory); + } + if (result instanceof InitializingBean) { + try { + ((InitializingBean) result).afterPropertiesSet(); + } catch (Exception e) { + throw new BeanInitializationException("failed to initialize annotated component", e); + } + } + if (result instanceof Lifecycle) { + lifecycles.add((Lifecycle) result); + if (result instanceof SmartLifecycle && ((SmartLifecycle) result).isAutoStartup()) { + ((SmartLifecycle) result).start(); + } + } + if (result instanceof ApplicationListener) { + listeners.add((ApplicationListener) result); } - } - if (result instanceof ApplicationListener) { - listeners.add((ApplicationListener) result); } } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandler.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandler.java index 15c90a32..0a045852 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandler.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandler.java @@ -15,6 +15,7 @@ */ package org.springframework.statemachine.processor; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import org.springframework.core.Ordered; @@ -28,15 +29,16 @@ import org.springframework.statemachine.annotation.WithStateMachine; * * @author Janne Valkealahti * + * @param the type of annotation * @param the type of state * @param the type of event */ -public class StateMachineHandler implements Ordered { +public class StateMachineHandler implements Ordered { private final Class beanClass; - private final StateMachineRuntimeProcessor processor; - + private final T metaAnnotation; + private final Annotation annotation; private int order = Ordered.LOWEST_PRECEDENCE; /** @@ -44,10 +46,12 @@ public class StateMachineHandler implements Ordered { * * @param beanClass the bean class * @param target the target bean + * @param metaAnnotation the meta annotation + * @param annotation the annotation * @param method the method */ - public StateMachineHandler(Class beanClass, Object target, Method method) { - this(beanClass, new MethodInvokingStateMachineRuntimeProcessor(target, method)); + public StateMachineHandler(Class beanClass, Object target, Method method, T metaAnnotation, Annotation annotation) { + this(beanClass, metaAnnotation, annotation, new MethodInvokingStateMachineRuntimeProcessor(target, method)); } /** @@ -56,31 +60,55 @@ public class StateMachineHandler implements Ordered { * @param beanClass the bean class * @param target the target bean * @param methodName the method name + * @param metaAnnotation the meta annotation + * @param annotation the annotation */ - public StateMachineHandler(Class beanClass, Object target, String methodName) { - this(beanClass, new MethodInvokingStateMachineRuntimeProcessor(target, methodName)); + public StateMachineHandler(Class beanClass, Object target, String methodName, T metaAnnotation, Annotation annotation) { + this(beanClass, metaAnnotation, annotation, new MethodInvokingStateMachineRuntimeProcessor(target, methodName)); } /** * Instantiates a new container handler. * - * @param the generic type * @param beanClass the bean class + * @param metaAnnotation the meta annotation + * @param annotation the annotation * @param processor the processor */ - public StateMachineHandler(Class beanClass, MethodInvokingStateMachineRuntimeProcessor processor) { + public StateMachineHandler(Class beanClass, T metaAnnotation, Annotation annotation, + MethodInvokingStateMachineRuntimeProcessor processor) { this.beanClass = beanClass; this.processor = processor; + this.metaAnnotation = metaAnnotation; + this.annotation = annotation; } @Override public int getOrder() { return order; } - + + /** + * Gets the meta annotation. + * + * @return the meta annotation + */ + public T getMetaAnnotation() { + return metaAnnotation; + } + + /** + * Gets the annotation. + * + * @return the annotation + */ + public Annotation getAnnotation() { + return annotation; + } + /** * Gets the bean class. - * + * * @return the bean class */ public Class getBeanClass() { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java new file mode 100644 index 00000000..f1e26dca --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java @@ -0,0 +1,356 @@ +/* + * Copyright 2015 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.statemachine.processor; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.messaging.Message; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.annotation.OnEventNotAccepted; +import org.springframework.statemachine.annotation.OnExtendedStateChanged; +import org.springframework.statemachine.annotation.OnStateChanged; +import org.springframework.statemachine.annotation.OnStateEntry; +import org.springframework.statemachine.annotation.OnStateExit; +import org.springframework.statemachine.annotation.OnStateMachineError; +import org.springframework.statemachine.annotation.OnStateMachineStart; +import org.springframework.statemachine.annotation.OnStateMachineStop; +import org.springframework.statemachine.annotation.OnTransition; +import org.springframework.statemachine.annotation.OnTransitionEnd; +import org.springframework.statemachine.annotation.OnTransitionStart; +import org.springframework.statemachine.annotation.WithStateMachine; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.StateMachineUtils; +import org.springframework.statemachine.transition.Transition; +import org.springframework.util.Assert; + +/** + * Helper class which is used from a StateMachineObjectSupport to ease handling + * of StateMachineHandlers and provides needed caching so that a runtime calls + * are fast. Also provides dedicated methods for each annotated methods so that + * parameters are handled accordingly. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class StateMachineHandlerCallHelper implements InitializingBean, BeanFactoryAware { + + private final Log log = LogFactory.getLog(StateMachineHandlerCallHelper.class); + private final Map> cache = new HashMap<>(); + private ListableBeanFactory beanFactory; + + @SuppressWarnings("unchecked") + @Override + public void afterPropertiesSet() throws Exception { + if (!(beanFactory instanceof ListableBeanFactory)) { + log.info("Beanfactory is not instance of ListableBeanFactory, was " + beanFactory + " thus Disabling handlers."); + return; + } + for (StateMachineHandler handler : beanFactory.getBeansOfType(StateMachineHandler.class).values()) { + Annotation annotation = handler.getAnnotation(); + Annotation metaAnnotation = handler.getMetaAnnotation(); + WithStateMachine withStateMachine = AnnotationUtils.findAnnotation(handler.getBeanClass(), + WithStateMachine.class); + String statemachineBeanName = withStateMachine.name(); + String key = metaAnnotation.annotationType().getName() + statemachineBeanName; + List list = cache.get(key); + if (list == null) { + list = new ArrayList<>(); + cache.put(key, list); + } + list.add(new CacheEntry(handler, annotation, metaAnnotation)); + } + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.state(beanFactory instanceof ListableBeanFactory, + "Bean factory must be instance of ListableBeanFactory, was " + beanFactory); + this.beanFactory = (ListableBeanFactory)beanFactory; + } + + public void callOnStateChanged(String stateMachineId, Transition transition, Message message, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnStateChanged.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerSourceTargetMatch((String[]) AnnotationUtils.getValue(entry.metaAnnotation, "source"), + (String[]) AnnotationUtils.getValue(entry.metaAnnotation, "target"), entry.annotation, stateContext.getSource(), + stateContext.getTarget())) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnStateEntry(String stateMachineId, Transition transition, Message message, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnStateEntry.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerSourceTargetMatch((String[]) AnnotationUtils.getValue(entry.metaAnnotation, "source"), + (String[]) AnnotationUtils.getValue(entry.metaAnnotation, "target"), entry.annotation, stateContext.getSource(), + stateContext.getTarget())) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnStateExit(String stateMachineId, Transition transition, Message message, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnStateExit.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerSourceTargetMatch((String[]) AnnotationUtils.getValue(entry.metaAnnotation, "source"), + (String[]) AnnotationUtils.getValue(entry.metaAnnotation, "target"), entry.annotation, stateContext.getSource(), + stateContext.getTarget())) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnEventNotAccepted(String stateMachineId, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnEventNotAccepted.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + handlersList.add(entry.handler); + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + + public void callOnTransitionStart(String stateMachineId, Transition transition, Message message, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnTransitionStart.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerSourceTargetMatch((String[]) AnnotationUtils.getValue(entry.metaAnnotation, "source"), + (String[]) AnnotationUtils.getValue(entry.metaAnnotation, "target"), entry.annotation, transition.getSource(), + transition.getTarget())) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnTransition(String stateMachineId, Transition transition, Message message, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnTransition.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerSourceTargetMatch((String[]) AnnotationUtils.getValue(entry.metaAnnotation, "source"), + (String[]) AnnotationUtils.getValue(entry.metaAnnotation, "target"), entry.annotation, transition.getSource(), + transition.getTarget())) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnTransitionEnd(String stateMachineId, Transition transition, Message message, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnTransitionEnd.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerSourceTargetMatch((String[]) AnnotationUtils.getValue(entry.metaAnnotation, "source"), + (String[]) AnnotationUtils.getValue(entry.metaAnnotation, "target"), entry.annotation, transition.getSource(), + transition.getTarget())) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnStateMachineStart(String stateMachineId, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnStateMachineStart.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + handlersList.add(entry.handler); + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnStateMachineStop(String stateMachineId, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnStateMachineStop.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + handlersList.add(entry.handler); + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnStateMachineError(String stateMachineId, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnStateMachineError.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + handlersList.add(entry.handler); + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + public void callOnExtendedStateChanged(String stateMachineId, Object key, Object value, StateContext stateContext) { + List> handlersList = new ArrayList>(); + String cacheKey = OnExtendedStateChanged.class.getName() + stateMachineId; + List list = cache.get(cacheKey); + if (list == null) { + return; + } + for (CacheEntry entry : list) { + if (annotationHandlerVariableMatch(entry.metaAnnotation, key)) { + handlersList.add(entry.handler); + } + } + getStateMachineHandlerResults(handlersList, stateContext); + } + + private boolean annotationHandlerVariableMatch(Annotation annotation, Object key) { + boolean handle = false; + Map annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation); + Object object = annotationAttributes.get("key"); + Collection scoll = StateMachineUtils.toStringCollection(object); + if (!scoll.isEmpty()) { + if (StateMachineUtils.containsAtleastOne(scoll, StateMachineUtils.toStringCollection(key))) { + handle = true; + } + } else { + handle = true; + } + return handle; + } + + private boolean annotationHandlerSourceTargetMatch(String[] msources, String[] mtargets, Annotation methodAnnotation, + State sourceState, State targetState) { + Map annotationAttributes = AnnotationUtils.getAnnotationAttributes(methodAnnotation); + Object source = annotationAttributes.get("source"); + Object target = annotationAttributes.get("target"); + + Collection scoll = StateMachineUtils.toStringCollection(source); + if (scoll.isEmpty() && msources != null) { + scoll = Arrays.asList(msources); + } + Collection tcoll = StateMachineUtils.toStringCollection(target); + if (tcoll.isEmpty() && mtargets != null) { + tcoll = Arrays.asList(mtargets); + } + + boolean handle = false; + if (!scoll.isEmpty() && !tcoll.isEmpty()) { + if (sourceState != null + && targetState != null + && StateMachineUtils.containsAtleastOne(scoll, + StateMachineUtils.toStringCollection(sourceState.getIds())) + && StateMachineUtils.containsAtleastOne(tcoll, + StateMachineUtils.toStringCollection(targetState.getIds()))) { + handle = true; + } + } else if (!scoll.isEmpty()) { + if (sourceState != null + && StateMachineUtils.containsAtleastOne(scoll, + StateMachineUtils.toStringCollection(sourceState.getIds()))) { + handle = true; + } + } else if (!tcoll.isEmpty()) { + if (targetState != null + && StateMachineUtils.containsAtleastOne(tcoll, + StateMachineUtils.toStringCollection(targetState.getIds()))) { + handle = true; + } + } else if (scoll.isEmpty() && tcoll.isEmpty()) { + handle = true; + } + + return handle; + } + + private List getStateMachineHandlerResults(List> stateMachineHandlers, + final StateContext stateContext) { + StateMachineRuntime runtime = new StateMachineRuntime() { + @Override + public StateContext getStateContext() { + return stateContext; + } + }; + List results = new ArrayList(); + for (StateMachineHandler handler : stateMachineHandlers) { + results.add(handler.handle(runtime)); + } + return results; + } + + private class CacheEntry { + final StateMachineHandler handler; + final Annotation annotation; + final Annotation metaAnnotation; + + public CacheEntry(StateMachineHandler handler, Annotation annotation, Annotation metaAnnotation) { + this.handler = handler; + this.annotation = annotation; + this.metaAnnotation = metaAnnotation; + } + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java index 1160f1a9..95a9adc5 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java @@ -30,6 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.Advised; import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; @@ -39,7 +40,9 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.statemachine.ExtendedState; import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.annotation.EventHeaders; +import org.springframework.statemachine.annotation.ExtendedStateVariable; import org.springframework.statemachine.support.AbstractExpressionEvaluator; import org.springframework.statemachine.support.AnnotatedMethodFilter; import org.springframework.statemachine.support.FixedMethodFilter; @@ -431,17 +434,24 @@ public class StateMachineMethodInvokerHelper extends AbstractExpression if (annotationType.equals(EventHeaders.class)) { sb.append("headers"); + } else if (annotationType.equals(ExtendedStateVariable.class)) { + AnnotationAttributes annotationAttributes = AnnotationAttributes + .fromMap(AnnotationUtils.getAnnotationAttributes(mappingAnnotation)); + String key = annotationAttributes.getAliasedString("value", ExtendedStateVariable.class, null); + sb.append("variables.get('" + key + "')"); } } else if (ExtendedState.class.isAssignableFrom(parameterType)) { sb.append("extendedState"); + } else if (StateMachine.class.isAssignableFrom(parameterType)) { + sb.append("stateMachine"); } } if (hasUnqualifiedMapParameter) { if (targetParameterType != null && Map.class.isAssignableFrom(this.targetParameterType)) { throw new IllegalArgumentException( "Unable to determine payload matching parameter due to ambiguous Map typed parameters. " - + "Consider adding the @Payload and or @Headers annotations as appropriate."); + + "Consider adding the @EventHeaders and or @ExtendedStateVariable annotations as appropriate."); } } sb.append(")"); @@ -466,6 +476,14 @@ public class StateMachineMethodInvokerHelper extends AbstractExpression + annotation.annotationType().getName() + "]"); } match = annotation; + } else if (type.equals(ExtendedStateVariable.class)) { + if (match != null) { + throw new IllegalArgumentException( + "At most one parameter annotation can be provided for message mapping, " + + "but found two: [" + match.annotationType().getName() + "] and [" + + annotation.annotationType().getName() + "]"); + } + match = annotation; } } return match; @@ -496,6 +514,14 @@ public class StateMachineMethodInvokerHelper extends AbstractExpression return stateContext.getExtendedState(); } + public Map getVariables() { + return getExtendedState().getVariables(); + } + + public StateMachine getStateMachine() { + return stateContext.getStateMachine(); + } + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineOnTransitionHandler.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineOnTransitionHandler.java deleted file mode 100644 index 1d2d1734..00000000 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineOnTransitionHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2015 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.statemachine.processor; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - -import org.springframework.statemachine.annotation.OnTransition; - -/** - * Transition specific {@link StateMachineHandler}. - * - * @author Janne Valkealahti - * - * @param the type of state - * @param the type of event - */ -public class StateMachineOnTransitionHandler extends StateMachineHandler { - - private final OnTransition metaAnnotation; - private final Annotation annotation; - - /** - * Instantiates a new state machine on transition handler. - * - * @param beanClass the bean class - * @param target the target - * @param method the method - * @param metaAnnotation the meta annotation - * @param annotation the annotation - */ - public StateMachineOnTransitionHandler(Class beanClass, Object target, Method method, OnTransition metaAnnotation, Annotation annotation) { - super(beanClass, target, method); - this.metaAnnotation = metaAnnotation; - this.annotation = annotation; - } - - /** - * Gets the meta annotation. - * - * @return the meta annotation - */ - public OnTransition getMetaAnnotation() { - return metaAnnotation; - } - - /** - * Gets the annotation. - * - * @return the annotation - */ - public Annotation getAnnotation() { - return annotation; - } - -} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 124ba274..d600307d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -15,15 +15,12 @@ */ package org.springframework.statemachine.support; -import java.lang.annotation.Annotation; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.UUID; import org.apache.commons.logging.Log; @@ -31,9 +28,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.core.OrderComparator; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; @@ -45,12 +39,7 @@ import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.access.StateMachineAccess; import org.springframework.statemachine.access.StateMachineAccessor; import org.springframework.statemachine.access.StateMachineFunction; -import org.springframework.statemachine.annotation.OnTransition; -import org.springframework.statemachine.annotation.WithStateMachine; import org.springframework.statemachine.listener.StateMachineListener; -import org.springframework.statemachine.processor.StateMachineHandler; -import org.springframework.statemachine.processor.StateMachineOnTransitionHandler; -import org.springframework.statemachine.processor.StateMachineRuntime; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.state.AbstractState; import org.springframework.statemachine.state.ForkPseudoState; @@ -67,7 +56,6 @@ import org.springframework.statemachine.transition.TransitionKind; import org.springframework.statemachine.trigger.DefaultTriggerContext; import org.springframework.statemachine.trigger.Trigger; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -101,10 +89,6 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private volatile PseudoState history; - private final Map> handlers = new HashMap>(); - - private volatile boolean handlersInitialized; - private final Map, Transition> triggerToTransitionMap = new HashMap, Transition>(); private final List> triggerlessTransitions = new ArrayList>(); @@ -192,7 +176,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo public boolean sendEvent(Message event) { if (hasStateMachineError()) { // TODO: should we throw exception? - notifyEventNotAccepted(event); + notifyEventNotAccepted(event, buildStateContext(null, null, getRelayStateMachine())); return false; } @@ -200,18 +184,18 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo event = getStateMachineInterceptors().preEvent(event, this); } catch (Exception e) { log.info("Event " + event + " threw exception in interceptors, not accepting event"); - notifyEventNotAccepted(event); + notifyEventNotAccepted(event, buildStateContext(null, null, getRelayStateMachine())); return false; } if (isComplete() || !isRunning()) { - notifyEventNotAccepted(event); + notifyEventNotAccepted(event, buildStateContext(null, null, getRelayStateMachine())); return false; } boolean accepted = acceptEvent(event); stateMachineExecutor.execute(); if (!accepted) { - notifyEventNotAccepted(event); + notifyEventNotAccepted(event, buildStateContext(null, null, getRelayStateMachine())); } return accepted; } @@ -232,7 +216,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo extendedState.setExtendedStateChangeListener(new ExtendedStateChangeListener() { @Override public void changed(Object key, Object value) { - notifyExtendedStateChanged(key, value); + notifyExtendedStateChanged(key, value, buildStateContext(null, null, getRelayStateMachine())); } }); @@ -277,16 +261,17 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo @Override public void transit(Transition t, StateContext stateContext, Message queuedMessage) { - notifyTransitionStart(t); - callHandlers(t.getSource(), t.getTarget(), queuedMessage); + StateContext stateContext2 = buildStateContext(queuedMessage, null, getRelayStateMachine()); + notifyTransitionStart(t, queuedMessage, buildStateContext(queuedMessage, null, getRelayStateMachine())); + notifyTransition(t, queuedMessage, buildStateContext(queuedMessage, null, getRelayStateMachine())); if (t.getKind() == TransitionKind.INITIAL) { switchToState(t.getTarget(), queuedMessage, t, getRelayStateMachine()); - notifyStateMachineStarted(getRelayStateMachine()); + notifyStateMachineStarted(getRelayStateMachine(), stateContext2); } else if (t.getKind() != TransitionKind.INTERNAL) { switchToState(t.getTarget(), queuedMessage, t, getRelayStateMachine()); } - notifyTransition(t); - notifyTransitionEnd(t); + // TODO: looks like events should be called here and anno processing earlier + notifyTransitionEnd(t, queuedMessage, buildStateContext(queuedMessage, null, getRelayStateMachine())); } }); stateMachineExecutor = executor; @@ -307,6 +292,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo @Override protected void doStart() { + super.doStart(); // if state is set assume nothing to do if (currentState != null) { if (log.isDebugEnabled()) { @@ -317,7 +303,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo // assume that state was set/reseted so we need to // dispatch started event which would net getting // dispatched via executor - notifyStateMachineStarted(getRelayStateMachine()); + StateContext stateContext = buildStateContext(null, null, getRelayStateMachine()); + notifyStateMachineStarted(getRelayStateMachine(), stateContext); return; } registerPseudoStateListener(); @@ -338,7 +325,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo @Override protected void doStop() { stateMachineExecutor.stop(); - notifyStateMachineStopped(this); + notifyStateMachineStopped(this, buildStateContext(null, null, this)); currentState = null; initialEnabled = null; } @@ -359,7 +346,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo currentError = exception; } if (currentError != null) { - notifyStateMachineError(this, currentError); + notifyStateMachineError(this, currentError, buildStateContext(null, null, this)); } } @@ -697,6 +684,13 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return new DefaultStateContext(event, messageHeaders, extendedState, transition, stateMachine); } + private StateContext buildStateContext(Message message, Transition transition, StateMachine stateMachine, State source, State target) { + E event = message != null ? message.getPayload() : null; + MessageHeaders messageHeaders = message != null ? message.getHeaders() : new MessageHeaders( + new HashMap()); + return new DefaultStateContext(event, messageHeaders, extendedState, transition, stateMachine, source, target); + } + private State findDeepParent(State state) { for (State s : states) { if (s.getStates().contains(state)) { @@ -729,7 +723,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo start(); } entryToState(state, message, transition, stateMachine); - notifyStateChanged(notifyFrom, state); + notifyStateChanged(notifyFrom, state, message, buildStateContext(message, null, getRelayStateMachine(), notifyFrom, state)); nonDeepStatePresent = true; } else if (currentState == null && StateMachineUtils.isSubstate(findDeep, state)) { if (exit) { @@ -741,7 +735,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo start(); } entryToState(findDeep, message, transition, stateMachine); - notifyStateChanged(notifyFrom, findDeep); + notifyStateChanged(notifyFrom, findDeep, message, buildStateContext(message, null, getRelayStateMachine(), notifyFrom, findDeep)); } if (currentState != null && !nonDeepStatePresent) { @@ -854,7 +848,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo log.debug("Exit state=[" + state + "]"); state.exit(stateContext); - notifyStateExited(state); + + notifyStateExited(state, message, buildStateContext(message, null, getRelayStateMachine(), state, null)); } private void entryToState(State state, Message message, Transition transition, StateMachine stateMachine) { @@ -882,115 +877,9 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } - notifyStateEntered(state); + notifyStateEntered(state, message, buildStateContext(message, null, getRelayStateMachine(), null, state)); log.debug("Enter state=[" + state + "]"); state.entry(stateContext); } - private void callHandlers(State sourceState, State targetState, Message message) { - StateContext stateContext = buildStateContext(message, null, getRelayStateMachine()); - getStateMachineHandlerResults(getStateMachineHandlers(sourceState, targetState), stateContext); - } - - private List getStateMachineHandlerResults(List> stateMachineHandlers, - final StateContext stateContext) { - StateMachineRuntime runtime = new StateMachineRuntime() { - @Override - public StateContext getStateContext() { - return stateContext; - } - }; - List results = new ArrayList(); - for (StateMachineHandler handler : stateMachineHandlers) { - results.add(handler.handle(runtime)); - } - return results; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private synchronized List> getStateMachineHandlers(State sourceState, - State targetState) { - BeanFactory beanFactory = getBeanFactory(); - - // TODO think how to handle null bf - if (beanFactory == null) { - return Collections.emptyList(); - } - Assert.state(beanFactory instanceof ListableBeanFactory, "Bean factory must be instance of ListableBeanFactory"); - - if (!handlersInitialized) { - Map handlersMap = ((ListableBeanFactory) beanFactory) - .getBeansOfType(StateMachineOnTransitionHandler.class); - for (Entry entry : handlersMap.entrySet()) { - handlers.put(entry.getKey(), entry.getValue()); - } - handlersInitialized = true; - } - - List> handlersList = new ArrayList>(); - - for (Entry> entry : handlers.entrySet()) { - // add only matching names from beanName and WithStateMachine name field - WithStateMachine withStateMachine = AnnotationUtils.findAnnotation(entry.getValue().getBeanClass(), WithStateMachine.class); - if (withStateMachine == null || !ObjectUtils.nullSafeEquals(withStateMachine.name(), getBeanName())) { - continue; - } - OnTransition metaAnnotation = entry.getValue().getMetaAnnotation(); - Annotation annotation = entry.getValue().getAnnotation(); - if (transitionHandlerMatch(metaAnnotation, annotation, sourceState, targetState)) { - handlersList.add(entry.getValue()); - } - } - - OrderComparator comparator = new OrderComparator(); - Collections.sort(handlersList, comparator); - return handlersList; - } - - private boolean transitionHandlerMatch(OnTransition metaAnnotation, Annotation annotation, State sourceState, State targetState) { - String[] msources = metaAnnotation.source(); - String[] mtargets = metaAnnotation.target(); - - Map annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation); - Object source = annotationAttributes.get("source"); - Object target = annotationAttributes.get("target"); - - Collection scoll = StateMachineUtils.toStringCollection(source); - if (scoll.isEmpty()) { - scoll = Arrays.asList(msources); - } - Collection tcoll = StateMachineUtils.toStringCollection(target); - if (tcoll.isEmpty()) { - tcoll = Arrays.asList(mtargets); - } - - boolean handle = false; - if (!scoll.isEmpty() && !tcoll.isEmpty()) { - if (sourceState != null - && targetState != null - && StateMachineUtils.containsAtleastOne(scoll, - StateMachineUtils.toStringCollection(sourceState.getIds())) - && StateMachineUtils.containsAtleastOne(tcoll, - StateMachineUtils.toStringCollection(targetState.getIds()))) { - handle = true; - } - } else if (!scoll.isEmpty()) { - if (sourceState != null - && StateMachineUtils.containsAtleastOne(scoll, - StateMachineUtils.toStringCollection(sourceState.getIds()))) { - handle = true; - } - } else if (!tcoll.isEmpty()) { - if (targetState != null - && StateMachineUtils.containsAtleastOne(tcoll, - StateMachineUtils.toStringCollection(targetState.getIds()))) { - handle = true; - } - } else if (scoll.isEmpty() && tcoll.isEmpty()) { - handle = true; - } - - return handle; - } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java index e11eeccf..4ed1032e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java @@ -15,37 +15,48 @@ */ package org.springframework.statemachine.support; +import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.ExtendedState; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; public class DefaultStateContext implements StateContext { private final E event; - private final MessageHeaders messageHeaders; - private final ExtendedState extendedState; - private final Transition transition; - private final StateMachine stateMachine; + private final State source; + private final State target; public DefaultStateContext(E event, MessageHeaders messageHeaders, ExtendedState extendedState, Transition transition, StateMachine stateMachine) { + this(event, messageHeaders, extendedState, transition, stateMachine, null, null); + } + + public DefaultStateContext(E event, MessageHeaders messageHeaders, ExtendedState extendedState, Transition transition, StateMachine stateMachine, State source, State target) { this.event = event; this.messageHeaders = messageHeaders; this.extendedState = extendedState; this.transition = transition; this.stateMachine = stateMachine; + this.source = source; + this.target = target; } - + @Override public E getEvent() { return event; } + @Override + public Message getMessage() { + return null; + } + @Override public MessageHeaders getMessageHeaders() { return messageHeaders; @@ -76,4 +87,18 @@ public class DefaultStateContext implements StateContext { return stateMachine; } + @Override + public State getSource() { + return source; + } + + @Override + public State getTarget() { + return target; + } + + @Override + public Exception getException() { + return null; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java index 27b80c51..56b6e0b6 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java @@ -23,10 +23,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanNameAware; import org.springframework.core.OrderComparator; import org.springframework.messaging.Message; +import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.event.StateMachineEventPublisher; import org.springframework.statemachine.listener.CompositeStateMachineListener; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.processor.StateMachineHandlerCallHelper; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; import org.springframework.util.Assert; @@ -51,10 +53,25 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup /** Flag for application context events */ private boolean contextEventsEnabled = true; - private final StateMachineInterceptorList interceptors = - new StateMachineInterceptorList(); - + private final StateMachineInterceptorList interceptors = new StateMachineInterceptorList(); private String beanName; + private volatile boolean handlersInitialized; + private final StateMachineHandlerCallHelper stateMachineHandlerCallHelper = new StateMachineHandlerCallHelper(); + + @Override + protected void doStart() { + super.doStart(); + if (!handlersInitialized) { + try { + stateMachineHandlerCallHelper.setBeanFactory(getBeanFactory()); + stateMachineHandlerCallHelper.afterPropertiesSet(); + } catch (Exception e) { + log.error("Unable to initialize annotation handlers", e); + } finally { + handlersInitialized = true; + } + } + } @Override public void setBeanName(String name) { @@ -111,7 +128,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup return stateListener; } - protected void notifyStateChanged(State source, State target) { + protected void notifyStateChanged(State source, State target, Message message, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnStateChanged(getBeanName(), null, message, stateContext); stateListener.stateChanged(source, target); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -121,7 +139,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyStateEntered(State state) { + protected void notifyStateEntered(State state, Message message, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnStateEntry(getBeanName(), null, message, stateContext); stateListener.stateEntered(state); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -131,7 +150,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyStateExited(State state) { + protected void notifyStateExited(State state, Message message, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnStateExit(getBeanName(), null, message, stateContext); stateListener.stateExited(state); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -141,7 +161,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyEventNotAccepted(Message event) { + protected void notifyEventNotAccepted(Message event, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnEventNotAccepted(getBeanName(), stateContext); stateListener.eventNotAccepted(event); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -151,7 +172,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyTransitionStart(Transition transition) { + protected void notifyTransitionStart(Transition transition, Message message, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnTransitionStart(getBeanName(), transition, message, stateContext); stateListener.transitionStarted(transition); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -161,7 +183,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyTransition(Transition transition) { + protected void notifyTransition(Transition transition, Message message, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnTransition(getBeanName(), transition, message, stateContext); stateListener.transition(transition); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -171,7 +194,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyTransitionEnd(Transition transition) { + protected void notifyTransitionEnd(Transition transition, Message message, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnTransitionEnd(getBeanName(), transition, message, stateContext); stateListener.transitionEnded(transition); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -181,7 +205,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyStateMachineStarted(StateMachine stateMachine) { + protected void notifyStateMachineStarted(StateMachine stateMachine, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnStateMachineStart(getBeanName(), stateContext); stateListener.stateMachineStarted(stateMachine); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -191,7 +216,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyStateMachineStopped(StateMachine stateMachine) { + protected void notifyStateMachineStopped(StateMachine stateMachine, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnStateMachineStop(getBeanName(), stateContext); stateListener.stateMachineStopped(stateMachine); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -201,7 +227,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyStateMachineError(StateMachine stateMachine, Exception exception) { + protected void notifyStateMachineError(StateMachine stateMachine, Exception exception, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnStateMachineError(getBeanName(), stateContext); stateListener.stateMachineError(stateMachine, exception); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); @@ -211,7 +238,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } - protected void notifyExtendedStateChanged(Object key, Object value) { + protected void notifyExtendedStateChanged(Object key, Object value, StateContext stateContext) { + stateMachineHandlerCallHelper.callOnExtendedStateChanged(getBeanName(), key, value, stateContext); stateListener.extendedStateChanged(key, value); if (contextEventsEnabled) { StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java index 077aae6c..0073a291 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java @@ -24,6 +24,8 @@ import java.util.Collection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.action.Action; @@ -75,8 +77,10 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { transitions.add(transitionFromS2ToS3); SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start(); @@ -148,8 +152,10 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { // create machine SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start(); @@ -189,8 +195,10 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { transitions.add(transitionInternalSI); SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java index ec4ff4fd..6d9a6484 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java @@ -29,6 +29,8 @@ import java.util.Collection; import java.util.concurrent.TimeUnit; import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -102,9 +104,11 @@ public class RegionMachineTests extends AbstractStateMachineTests { transitions.add(transitionFromS2ToS3); SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); Transition initialTransition = new InitialTransition(stateSI); ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateSI, initialTransition, null, null); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start(); @@ -131,6 +135,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { @Test public void testMultiRegionBuildRaw() throws Exception { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); State stateSI = new EnumState(TestStates.SI, pseudoState); @@ -169,6 +174,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { Transition initialTransition11 = new InitialTransition(stateS111); ObjectStateMachine machine11 = new ObjectStateMachine(states11, transitions11, stateS111, initialTransition11, null, null); machine11.setTaskExecutor(taskExecutor); + machine11.setBeanFactory(beanFactory); machine11.afterPropertiesSet(); Collection> states12 = new ArrayList>(); @@ -181,6 +187,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { Transition initialTransition12 = new InitialTransition(stateS121); ObjectStateMachine machine12 = new ObjectStateMachine(states12, transitions12, stateS121, initialTransition12, null, null); machine12.setTaskExecutor(taskExecutor); + machine12.setBeanFactory(beanFactory); machine12.afterPropertiesSet(); Collection> regions = new ArrayList>(); @@ -198,6 +205,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateR, initialTransition, null, null); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java index f4437e03..e34d6f30 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java @@ -27,6 +27,8 @@ import java.util.Collection; import java.util.concurrent.TimeUnit; import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -128,11 +130,15 @@ public class SubStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); submachine1.setTaskExecutor(taskExecutor); + submachine1.setBeanFactory(beanFactory); submachine1.afterPropertiesSet(); submachine11.setTaskExecutor(taskExecutor); + submachine11.setBeanFactory(beanFactory); submachine11.afterPropertiesSet(); machine.start(); @@ -220,9 +226,12 @@ public class SubStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); submachine11.setTaskExecutor(taskExecutor); + submachine11.setBeanFactory(beanFactory); submachine11.afterPropertiesSet(); machine.start(); @@ -318,11 +327,15 @@ public class SubStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); submachine1.setTaskExecutor(taskExecutor); + submachine1.setBeanFactory(beanFactory); submachine1.afterPropertiesSet(); submachine11.setTaskExecutor(taskExecutor); + submachine11.setBeanFactory(beanFactory); submachine11.afterPropertiesSet(); machine.start(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java index 1bcd6f6f..755659f8 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java @@ -32,7 +32,10 @@ import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.ExtendedState; import org.springframework.statemachine.ObjectStateMachine; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; @@ -40,39 +43,124 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionCo public class MethodAnnotationTests extends AbstractStateMachineTests { + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + @Test @SuppressWarnings("unchecked") - public void testMethodAnnotations() throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BaseConfig.class, BeanConfig1.class, Config1.class); + public void testOnTransition() throws Exception { + context.register(BaseConfig.class, BeanConfig1.class, Config1.class); + context.refresh(); ObjectStateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); assertThat(context.containsBean("fooMachine"), is(true)); Bean1 bean1 = context.getBean(Bean1.class); + + bean1.reset(1, 1, 1, 1, 1, 1, 1, 1); machine.start(); - assertThat(bean1.onMethod1Latch.await(2, TimeUnit.SECONDS), is(false)); - assertThat(bean1.onOnTransitionLatch.await(2, TimeUnit.SECONDS), is(true)); - assertThat(bean1.onMethod1Count, is(0)); - assertThat(bean1.onOnTransitionCount, is(1)); + assertThat(bean1.onTransitionFromS1ToS2Latch.await(1, TimeUnit.SECONDS), is(false)); + assertThat(bean1.onTransitionLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onTransitionFromS1ToS2Count, is(0)); + assertThat(bean1.onTransitionCount, is(1)); - bean1.reset(1, 1); - - // this event should cause 'method1' to get called + bean1.reset(1, 1, 1, 1, 1, 1, 1, 1); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); - assertThat(bean1.onMethod1Latch.await(2, TimeUnit.SECONDS), is(true)); - assertThat(bean1.onOnTransitionLatch.await(2, TimeUnit.SECONDS), is(true)); - assertThat(bean1.onMethod1Count, is(1)); - assertThat(bean1.onOnTransitionCount, is(1)); + assertThat(bean1.onTransitionFromS1ToS2Latch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onTransitionLatch.await(1, TimeUnit.SECONDS), is(true)); - context.close(); + assertThat(bean1.onTransitionFromS1ToS2Count, is(1)); + assertThat(bean1.onTransitionCount, is(1)); + } + + @Test + @SuppressWarnings("unchecked") + public void testOnStateChanged() throws Exception { + context.register(BaseConfig.class, BeanConfig1.class, Config1.class); + context.refresh(); + + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(context.containsBean("fooMachine"), is(true)); + Bean1 bean1 = context.getBean(Bean1.class); + + bean1.reset(1, 1, 1, 1, 1, 1, 1, 1); + machine.start(); + + assertThat(bean1.onStateChangedFromS1ToS2Latch.await(1, TimeUnit.SECONDS), is(false)); + assertThat(bean1.onStateChangedLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onStateChangedFromS1ToS2Count, is(0)); + assertThat(bean1.onStateChangedCount, is(1)); + + bean1.reset(1, 1, 1, 1, 1, 1, 1, 1); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + + assertThat(bean1.onStateChangedFromS1ToS2Latch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onStateChangedLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onStateChangedFromS1ToS2Count, is(1)); + assertThat(bean1.onStateChangedCount, is(1)); + } + + @Test + @SuppressWarnings("unchecked") + public void testOnStateMachineStartStop() throws Exception { + context.register(BaseConfig.class, BeanConfig1.class, Config1.class); + context.refresh(); + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(context.containsBean("fooMachine"), is(true)); + Bean1 bean1 = context.getBean(Bean1.class); + + bean1.reset(1, 1, 1, 1, 1, 1, 1, 1); + + machine.start(); + assertThat(bean1.onStateMachineStartLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onStateMachineStartCount, is(1)); + assertThat(bean1.onStateMachineStopLatch.await(1, TimeUnit.SECONDS), is(false)); + assertThat(bean1.onStateMachineStopCount, is(0)); + + bean1.reset(1, 1, 1, 1, 1, 1, 1, 1); + machine.stop(); + assertThat(bean1.onStateMachineStartLatch.await(1, TimeUnit.SECONDS), is(false)); + assertThat(bean1.onStateMachineStartCount, is(0)); + assertThat(bean1.onStateMachineStopLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onStateMachineStopCount, is(1)); + } + + @Test + @SuppressWarnings("unchecked") + public void testOnExtendedStateChanged() throws Exception { + context.register(BaseConfig.class, BeanConfig5.class, Config1.class); + context.refresh(); + + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(context.containsBean("fooMachine"), is(true)); + Bean5 bean5 = context.getBean(Bean5.class); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("V1", "V1val").build()); + + assertThat(bean5.onExtendedStateChanged1Latch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean5.onExtendedStateChanged1Count, is(1)); + + assertThat(bean5.onExtendedStateChanged2Latch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(bean5.onExtendedStateChanged2Count, is(1)); + assertThat(bean5.onExtendedStateChanged2Value, is("V1val")); + + assertThat(bean5.onExtendedStateChangedKeyV2Latch.await(1, TimeUnit.SECONDS), is(false)); + assertThat(bean5.onExtendedStateChangedKeyV2Count, is(0)); } @Test @SuppressWarnings("unchecked") public void testMethodAnnotations2() throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BaseConfig.class, BeanConfig2.class, Config1.class); + context.register(BaseConfig.class, BeanConfig2.class, Config1.class); + context.refresh(); ObjectStateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); @@ -93,35 +181,161 @@ public class MethodAnnotationTests extends AbstractStateMachineTests { assertThat(bean2.onMethod2Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean2.variable, notNullValue()); assertThat((String)bean2.variable, is("jee")); + } - context.close(); + @Test + @SuppressWarnings("unchecked") + public void testMethodAnnotations3() throws Exception { + context.register(BaseConfig.class, BeanConfig3.class, Config1.class); + context.refresh(); + + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(context.containsBean("fooMachine"), is(true)); + machine.start(); + + Bean3 bean3 = context.getBean(Bean3.class); + + // this event should cause 'method1' to get called + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + + assertThat(bean3.onStateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + } + + @Test + @SuppressWarnings("unchecked") + public void testMethodAnnotations4() throws Exception { + context.register(BaseConfig.class, BeanConfig4.class, Config1.class); + context.refresh(); + + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(context.containsBean("fooMachine"), is(true)); + Bean4 bean4 = context.getBean(Bean4.class); + machine.start(); + + assertThat(bean4.onStateEntryLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean4.onStateExitLatch.await(2, TimeUnit.SECONDS), is(false)); + assertThat(bean4.onStateEntryCount, is(1)); + assertThat(bean4.onStateExitCount, is(0)); + + bean4.reset(1, 1); + + // this event should cause 'method1' to get called + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(bean4.onStateEntryLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean4.onStateExitLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean4.onStateEntryCount, is(1)); + assertThat(bean4.onStateExitCount, is(1)); + } + + @Test + @SuppressWarnings("unchecked") + public void testMethodAnnotations5() throws Exception { + context.register(BaseConfig.class, BeanConfig6.class, Config1.class); + context.refresh(); + + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + Bean6 bean6 = context.getBean(Bean6.class); + machine.start(); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E4).build()); + assertThat(bean6.onEventNotAcceptedLatch.await(2, TimeUnit.SECONDS), is(true)); } @WithStateMachine static class Bean1 { - - CountDownLatch onMethod1Latch = new CountDownLatch(1); - CountDownLatch onOnTransitionLatch = new CountDownLatch(1); - int onMethod1Count; - int onOnTransitionCount; + CountDownLatch onTransitionFromS1ToS2Latch = new CountDownLatch(1); + CountDownLatch onTransitionLatch = new CountDownLatch(1); + CountDownLatch onStateChangedFromS1ToS2Latch = new CountDownLatch(1); + CountDownLatch onStateChangedLatch = new CountDownLatch(1); + CountDownLatch onTransitionEndLatch = new CountDownLatch(1); + CountDownLatch onTransitionStartLatch = new CountDownLatch(1); + CountDownLatch onStateMachineStartLatch = new CountDownLatch(1); + CountDownLatch onStateMachineStopLatch = new CountDownLatch(1); + int onTransitionFromS1ToS2Count = 0; + int onTransitionCount = 0; + int onStateChangedFromS1ToS2Count = 0; + int onStateChangedCount = 0; + int onTransitionEndCount = 0; + int onTransitionStartCount = 0; + int onStateMachineStartCount = 0; + int onStateMachineStopCount = 0; @OnTransition(source = "S1", target = "S2") - public void method1() { - onMethod1Count++; - onMethod1Latch.countDown(); + public void onTransitionFromS1ToS2() { + onTransitionFromS1ToS2Count++; + onTransitionFromS1ToS2Latch.countDown(); } @OnTransition public void onTransition() { - onOnTransitionCount++; - onOnTransitionLatch.countDown(); + onTransitionCount++; + onTransitionLatch.countDown(); } - public void reset(int a1, int a2) { - onMethod1Latch = new CountDownLatch(a1); - onOnTransitionLatch = new CountDownLatch(a2); - onMethod1Count = 0; - onOnTransitionCount = 0; + @OnTransitionEnd + public void onTransitionEnd() { + onTransitionEndCount++; + onTransitionEndLatch.countDown(); + } + + @OnTransitionStart + public void onTransitionStart() { + onTransitionStartCount++; + onTransitionStartLatch.countDown(); + } + + @OnStateChanged(source = "S1", target = "S2") + public void onStateChangedFromS1ToS2() { + onStateChangedFromS1ToS2Count++; + onStateChangedFromS1ToS2Latch.countDown(); + } + + @OnStateChanged + public void onStateChanged() { + onStateChangedCount++; + onStateChangedLatch.countDown(); + } + + @OnStateMachineStart + public void onStateMachineStart() { + onStateMachineStartLatch.countDown(); + onStateMachineStartCount++; + } + + @OnStateMachineStart + public void onStateMachineStartWithParam(StateMachine machine) { + } + + @OnStateMachineStop + public void onStateMachineStop() { + onStateMachineStopLatch.countDown(); + onStateMachineStopCount++; + } + + @OnStateMachineStop + public void onStateMachineStopWithParam(StateMachine machine) { + } + + public void reset(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) { + onTransitionFromS1ToS2Latch = new CountDownLatch(a1); + onTransitionLatch = new CountDownLatch(a2); + onStateChangedFromS1ToS2Latch = new CountDownLatch(a3); + onStateChangedLatch = new CountDownLatch(a4); + onTransitionEndLatch = new CountDownLatch(a5); + onTransitionStartLatch = new CountDownLatch(a6); + onStateMachineStartLatch = new CountDownLatch(a7); + onStateMachineStopLatch = new CountDownLatch(a8); + onTransitionFromS1ToS2Count = 0; + onTransitionCount = 0; + onStateChangedFromS1ToS2Count = 0; + onStateChangedCount = 0; + onTransitionEndCount = 0; + onTransitionStartCount = 0; + onStateMachineStartCount = 0; + onStateMachineStopCount = 0; } } @@ -151,6 +365,100 @@ public class MethodAnnotationTests extends AbstractStateMachineTests { } + @WithStateMachine + static class Bean3 { + + CountDownLatch onStateChangedLatch = new CountDownLatch(1); + + @OnStateChanged + public void onStateChanged() { + onStateChangedLatch.countDown(); + } + + } + + @WithStateMachine + static class Bean4 { + + CountDownLatch onStateEntryLatch = new CountDownLatch(1); + CountDownLatch onStateExitLatch = new CountDownLatch(1); + int onStateEntryCount = 0; + int onStateExitCount = 0; + + @OnStateEntry + public void onStateEntry() { + onStateEntryCount++; + onStateEntryLatch.countDown(); + } + + @OnStateExit + public void onStateExit() { + onStateExitCount++; + onStateExitLatch.countDown(); + } + + public void reset(int a1, int a2) { + onStateEntryCount = 0; + onStateExitCount = 0; + onStateEntryLatch = new CountDownLatch(a1); + onStateExitLatch = new CountDownLatch(a2); + } + + } + + @WithStateMachine + static class Bean5 { + + CountDownLatch onExtendedStateChanged1Latch = new CountDownLatch(1); + CountDownLatch onExtendedStateChanged2Latch = new CountDownLatch(1); + CountDownLatch onExtendedStateChangedKeyV2Latch = new CountDownLatch(1); + int onExtendedStateChanged1Count = 0; + int onExtendedStateChanged2Count = 0; + Object onExtendedStateChanged2Value = null; + int onExtendedStateChangedKeyV2Count = 0; + + @OnExtendedStateChanged + public void onExtendedStateChanged1() { + onExtendedStateChanged1Count++; + onExtendedStateChanged1Latch.countDown(); + } + + @OnExtendedStateChanged + public void onExtendedStateChanged2(@ExtendedStateVariable("V1") Object value) { + onExtendedStateChanged2Value = value; + onExtendedStateChanged2Count++; + onExtendedStateChanged2Latch.countDown(); + } + + @OnExtendedStateChanged(key = "V2") + public void onExtendedStateChangedKeyV2() { + onExtendedStateChangedKeyV2Count++; + onExtendedStateChangedKeyV2Latch.countDown(); + } + + public void reset(int a1, int a2, int a3) { + onExtendedStateChanged1Latch = new CountDownLatch(a1); + onExtendedStateChanged2Latch = new CountDownLatch(a2); + onExtendedStateChangedKeyV2Latch = new CountDownLatch(a3); + onExtendedStateChanged1Count = 0; + onExtendedStateChanged2Count = 0; + onExtendedStateChanged2Value = null; + onExtendedStateChangedKeyV2Count = 0; + } + + } + + @WithStateMachine + static class Bean6 { + CountDownLatch onEventNotAcceptedLatch = new CountDownLatch(1); + + @OnEventNotAccepted + public void onEventNotAcceptedLatch() { + onEventNotAcceptedLatch.countDown(); + } + + } + @Configuration static class BeanConfig1 { @@ -171,6 +479,46 @@ public class MethodAnnotationTests extends AbstractStateMachineTests { } + @Configuration + static class BeanConfig3 { + + @Bean + public Bean3 bean3() { + return new Bean3(); + } + + } + + @Configuration + static class BeanConfig4 { + + @Bean + public Bean4 bean4() { + return new Bean4(); + } + + } + + @Configuration + static class BeanConfig5 { + + @Bean + public Bean5 bean5() { + return new Bean5(); + } + + } + + @Configuration + static class BeanConfig6 { + + @Bean + public Bean6 bean6() { + return new Bean6(); + } + + } + @Configuration @EnableStateMachine(name = {StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, "fooMachine"}) static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -192,6 +540,7 @@ public class MethodAnnotationTests extends AbstractStateMachineTests { .event(TestEvents.E1) .guard(testGuard()) .action(testAction()) + .action(extendedStateAction()) .and() .withExternal() .source(TestStates.S2) @@ -214,6 +563,20 @@ public class MethodAnnotationTests extends AbstractStateMachineTests { return new TestAction(); } + @Bean + public Action extendedStateAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + String e1 = context.getMessageHeaders().get("V1", String.class); + if (e1 != null) { + context.getExtendedState().getVariables().put("V1", e1); + } + } + }; + } + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java index ac584243..a7e9b14f 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java @@ -363,7 +363,6 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { } - @Configuration @EnableStateMachine static class Config4 extends EnumStateMachineConfigurerAdapter { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java index d9deb218..ec9316be 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java @@ -23,6 +23,8 @@ import java.util.ArrayList; import java.util.Collection; import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.ObjectStateMachine; @@ -69,8 +71,10 @@ public class RegionStateTests extends AbstractStateMachineTests { transitions.add(transitionFromS2ToS3); SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java index e16ffd83..d2ff5cd4 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java @@ -24,6 +24,8 @@ import java.util.ArrayList; import java.util.Collection; import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SyncTaskExecutor; @@ -83,8 +85,10 @@ public class SubmachineStateTests extends AbstractStateMachineTests { transitions.add(transitionFromS2ToS3); SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); + BeanFactory beanFactory = new DefaultListableBeanFactory(); ObjectStateMachine machine = new ObjectStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.setBeanFactory(beanFactory); machine.afterPropertiesSet(); machine.start();