-
Notifications
You must be signed in to change notification settings - Fork 0
/
StateMachine.cfc
205 lines (161 loc) · 7.6 KB
/
StateMachine.cfc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
<cfcomponent output="false">
<cfset instance = structNew() />
<cffunction name="init" access="public" output="false">
<cfargument name="originalObject" required="true" />
<cfargument name="initialState" type="string" required="true" />
<cfargument name="stateMethod" type="string" required="false" default="state" />
<cfargument name="persistenceObject" required="false" default="" hint="a service that persist the business object" />
<cfargument name="persistenceMethod" required="false" default="save" />
<cfscript>
setOriginalObject(arguments.OriginalObject);
instance.states = structNew();
instance.transitionTable = structNew();
instance.eventTable = structNew();
instance.stateMethod = arguments.stateMethod;
instance.initialState = arguments.initialState;
instance.persistenceObject = arguments.persistenceObject;
instance.persistenceMethod = arguments.persistenceMethod;
configureState();
if(getState() eq '') {
setInitialState(instance.initialState);
}
return this;
</cfscript>
</cffunction>
<!--- Builds the state and event from the configuration --->
<cffunction name="addState" access="private" output="false">
<cfargument name="name" type="string" required="true" />
<cfset var state = createObject("component", "shade.State").init(arguments.name) />
<cfset instance.states[arguments.name] = state />
<cfreturn state />
</cffunction>
<cffunction name="addEvent" access="private" output="false">
<cfargument name="name" type="string" required="true" />
<cfset var event = createObject("component", "shade.Event").init(arguments.name, instance.transitionTable) />
<cfset instance.eventTable[arguments.name] = event />
<cfreturn event />
</cffunction>
<!--- Handles firing events --->
<cffunction name="fireEvent" access="public" output="false">
<cfargument name="name" type="string" required="true" />
<cfif structKeyExists(instance.eventTable, arguments.name)>
<cfreturn instance.eventTable[arguments.name].fire(this, instance.persistenceObject, instance.persistenceMethod) />
</cfif>
<cfreturn false />
</cffunction>
<!--- Holds a reference to the object that is being decorated --->
<cffunction name="getOriginalObject" access="public" output="false">
<cfreturn instance.OriginalObject />
</cffunction>
<cffunction name="setOriginalObject" access="public" output="false">
<cfargument name="object" required="true" />
<cfset instance.OriginalObject = arguments.object />
</cffunction>
<!--- Gets the current value of the state method from the decorated class --->
<cffunction name="getState" access="public" output="false">
<cfreturn invokeMethod("get#instance.stateMethod#") />
</cffunction>
<cffunction name="setInternalState" access="public" output="false">
<cfargument name="state" type="string" required="true" />
<cfset var local = structNew() />
<cfset local.state = arguments.state />
<cfset invokeMethod("set#instance.stateMethod#", local) />
</cffunction>
<!--- Accessors --->
<cffunction name="getInitialState" access="public" returntype="string" output="false">
<cfreturn instance.initialState />
</cffunction>
<!--- This should only be called once during init --->
<cffunction name="setInitialState" access="private" output="false">
<cfargument name="stateName" required="true" />
<cfscript>
var state = instance.states[arguments.stateName];
state.before(this);
setInternalState(arguments.stateName);
state.after(this);
</cfscript>
</cffunction>
<cffunction name="getCurrentState" access="public" returntype="string" output="false">
<cfreturn invokeMethod("get#instance.stateMethod#") />
</cffunction>
<cffunction name="getStateMethod" access="public" returntype="string" output="false">
<cfreturn instance.stateMethod />
</cffunction>
<cffunction name="getTransitionTable" access="public" returntype="struct" output="false">
<cfreturn instance.transitionTable />
</cffunction>
<cffunction name="getEventTable" access="public" returntype="string" output="false">
<cfreturn instance.eventTable />
</cffunction>
<cffunction name="getStates" access="public" returntype="struct" output="false">
<cfreturn instance.states />
</cffunction>
<cffunction name="isInState" access="public" returntype="boolean" output="false">
<cfargument name="state" type="string" required="true" />
<cfreturn state eq getCurrentState() />
</cffunction>
<!--- Returns what the next state for a given event would be, as a string. --->
<cffunction name="getNextStateForEvent" access="public" returntype="string" output="false">
<cfargument name="event" type="string" required="true" />
<cfset var transitions = instance.transitionTable[arguments.event] />
<cfset var transition = '' />
<cfset transitions.reset() />
<cfloop condition="transitions.hasNext()">
<cfset transition = transitions.next() />
<cfif transition.getFromState() eq getCurrentState()>
<cfreturn transition.getToState() />
</cfif>
</cfloop>
<cfreturn '' />
</cffunction>
<!---
We are using onMissingMethod for three things:
1. To defined the state query handlers, i.e. isClosed()
2. To define the event firing shortcuts, i.e. close()
3. To pass any unknown method on to the decorated object to handle
--->
<cffunction name="onMissingMethod" output="false" access="public">
<cfargument name="missingMethodName" type="string" />
<cfargument name="missingMethodArguments" type="struct" />
<!--- This is to highly discourage the manual setting of state.
It can still be done by getting the original object and
setting it directly. BAD. --->
<cfif arguments.missingMethodName is "set#getStateMethod()#" >
<cfthrow type="shade.InvalidStateChange" message="You cannot set the state directly. Please call an event instead to change the state." />
</cfif>
<!--- Shortcut to wire up event methods, so basically if you have a
'trash' event, you can call obj.trash() rather than the longer
obj.fireEvent('trash') --->
<cfif structKeyExists(instance.eventTable, arguments.missingMethodName)>
<cfreturn fireEvent(arguments.missingMethodName) />
</cfif>
<!--- Adds query methods. For and event 'trash', this will allow you to call isTrash() --->
<cfset stateList = structKeyList(instance.states, '|') />
<cfif refindnocase('^is(#stateList#)$', arguments.missingMethodName)>
<cfreturn isInState(mid(arguments.missingMethodName, 3, len(arguments.missingMethodName)-2)) />
</cfif>
<!--- Passthrough to decorated object --->
<cfreturn invokeMethod(arguments.missingMethodName, arguments.missingMethodArguments) />
</cffunction>
<!--- Utility --->
<cffunction name="invokeMethod" access="private" returntype="Any" output="false">
<cfargument name="method" type="string" required="true" />
<cfargument name="argcollection" type="struct" required="false" default="#structNew()#" />
<cfset var returnVar = "" />
<!--- runs cfinvoke on self to run an internal method --->
<cfinvoke component="#getOriginalObject()#" method="#arguments.method#" argumentcollection="#arguments.argcollection#" returnvariable="returnVar" />
<!--- returns the variables that cfinvoke work return --->
<!--- will only return a variables if one is present --->
<cfif isDefined("returnVar")>
<cfreturn returnVar />
</cfif>
</cffunction>
<cffunction name="dump" output="true" returntype="void">
<cfargument name="v" type="any" required="true"/>
<cfargument name="abort" type="boolean" default="false"/>
<cfdump var="#arguments.v#"/>
<cfif arguments.abort>
<cfabort />
</cfif>
</cffunction>
</cfcomponent>