From 5f7a0e95228ef4f633e88c43729682743e3e8413 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Mon, 4 Mar 2024 16:35:14 +0100 Subject: [PATCH 01/16] test: add testStateMachineVsStateMachineModelConsistency() --- .../uml/UmlStateMachineModelFactoryTests.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java index b1fab4020..1c38cdbca 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java @@ -21,9 +21,11 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -35,6 +37,7 @@ import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineBuilder; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineModelConfigurer; import org.springframework.statemachine.config.model.DefaultStateMachineComponentResolver; @@ -45,7 +48,10 @@ import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.PseudoStateKind; +import org.springframework.statemachine.state.RegionState; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.AbstractStateMachine; +import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionKind; import org.springframework.util.ObjectUtils; @@ -163,6 +169,109 @@ public void testSimpleRootRegions() { } } + /** + * Test {@link StateMachine} vs {@link StateMachineModel} consistency.
+ * In this (failing) test, one can notice that the statemachine instance has a duplicated transition "S3->S4" as illustrated here
+ * some + */ + @Disabled + @Test + public void testStateMachineVsStateMachineModelConsistency() { + context.refresh(); + Resource model1 = new ClassPathResource("org/springframework/statemachine/uml/simple-root-regions.uml"); + UmlStateMachineModelFactory builder = new UmlStateMachineModelFactory(model1); + builder.setBeanFactory(context); + assertThat(model1.exists()).isTrue(); + StateMachineModel stateMachineModel = builder.build(); + + try { + // build statemachine from model + UmlStateMachineModelFactory umlStateMachineModelFactory = new UmlStateMachineModelFactory(("classpath:org/springframework/statemachine/uml/simple-root-regions.uml")); + StateMachineBuilder.Builder stateMachineBuilder = StateMachineBuilder.builder(); + stateMachineBuilder.configureModel().withModel().factory(umlStateMachineModelFactory); + stateMachineBuilder.configureConfiguration().withConfiguration(); + StateMachine stateMachine = stateMachineBuilder.build(); + + // get the "root" state of this state machines + State rootState = stateMachine.getStates().stream().findFirst().get(); + assertThat(rootState).isInstanceOf(RegionState.class); + RegionState rootRegionState = ((RegionState) rootState); + + // compare statemachine and stateMachineModel + + // states in Region1 + AbstractStateMachine region1InStatemachine = (AbstractStateMachine) + ((List) rootRegionState.getRegions()).stream() + .filter(region -> ((AbstractStateMachine) region).getId().contains("Region1")) + .findFirst().get(); + + List statesOfRegion1InStateMachine = region1InStatemachine.getStates().stream() + .map(o -> ((State) o).getId().toString()) + .sorted().toList(); + + List statesOfRegion1InStateMachineModel = stateMachineModel.getStatesData().getStateData().stream() + .filter(stateData -> "Region1".equals(stateData.getRegion().toString())) + .map(stateData -> stateData.getState().toString()) + .sorted().toList(); + + assertThat(statesOfRegion1InStateMachine).isEqualTo(statesOfRegion1InStateMachineModel); + + // states in Region2 + AbstractStateMachine region2InStatemachine = (AbstractStateMachine) + ((List) rootRegionState.getRegions()).stream() + .filter(region -> ((AbstractStateMachine) region).getId().contains("Region2")) + .findFirst().get(); + + List statesOfRegion2InStateMachine = region2InStatemachine.getStates().stream() + .map(o -> ((State) o).getId().toString()) + .sorted().toList(); + + List statesOfRegion2InStateMachineModel = stateMachineModel.getStatesData().getStateData().stream() + .filter(stateData -> "Region2".equals(stateData.getRegion().toString())) + .map(stateData -> stateData.getState().toString()) + .sorted().toList(); + + assertThat(statesOfRegion2InStateMachine).isEqualTo(statesOfRegion2InStateMachineModel); + + // transitions in Region1 + List transitionsOfRegion1InStateMachine = region1InStatemachine.getTransitions().stream() + .map(o -> ((Transition) o).getSource().getId().toString() + "->" + ((Transition) o).getTarget().getId().toString()) + .sorted().toList(); + + List transitionsOfRegion1InStateMachineModel = stateMachineModel.getTransitionsData().getTransitions().stream() + // let's exclude "initial" transition + .filter(transitionData -> !transitionData.getSource().startsWith("initial")) + .filter(transitionData -> statesOfRegion1InStateMachine.contains(transitionData.getSource()) + || statesOfRegion1InStateMachine.contains(transitionData.getTarget())) + .map(transitionData -> transitionData.getSource() + "->" + transitionData.getTarget()) + .sorted().toList(); + + assertThat(transitionsOfRegion1InStateMachine).isEqualTo(transitionsOfRegion1InStateMachineModel); + + // transitions in Region2 + List transitionsOfRegion2InStateMachine = region2InStatemachine.getTransitions().stream() + .map(o -> ((Transition) o).getSource().getId().toString() + "->" + ((Transition) o).getTarget().getId().toString()) + .sorted().toList(); + + List transitionsOfRegion2InStateMachineModel = stateMachineModel.getTransitionsData().getTransitions().stream() + // let's exclude "initial" transition + .filter(transitionData -> !transitionData.getSource().startsWith("initial")) + .filter(transitionData -> statesOfRegion2InStateMachine.contains(transitionData.getSource()) + || statesOfRegion2InStateMachine.contains(transitionData.getTarget())) + .map(transitionData -> transitionData.getSource() + "->" + transitionData.getTarget()) + .sorted().toList(); + + // WOW! this is failing! Why is transition "S3->S4" present in both Region1 AND Region2 ?!? + // Does this indicates an issue in UmlStateMachineModelFactory ??? + // Expected :["S1->S2"] + // Actual :["S1->S2", "S3->S4"] + assertThat(transitionsOfRegion2InStateMachine).isEqualTo(transitionsOfRegion2InStateMachineModel); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Test public void testSimpleFlatEnd() { context.refresh(); From 79eb82a0c00c05ee01d9f738a00df7fe328c6521 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Mon, 4 Mar 2024 16:38:16 +0100 Subject: [PATCH 02/16] test: testStateMachineVsStateMachineModelConsistency() add png image to illustrate the issue --- .../statemachine/uml/simple-root-regions.png | Bin 0 -> 8199 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png new file mode 100644 index 0000000000000000000000000000000000000000..37ca638531c1b9aa7abf91677426b824dc1f3ca5 GIT binary patch literal 8199 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 Date: Wed, 6 Mar 2024 07:52:24 +0100 Subject: [PATCH 03/16] test: testStateMachineVsStateMachineModelConsistency() update png --- .../statemachine/uml/simple-root-regions.png | Bin 8199 -> 7946 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png index 37ca638531c1b9aa7abf91677426b824dc1f3ca5..7424c97bcf0ea10545b05174bc5b08169bbe59be 100644 GIT binary patch literal 7946 zcmd5>^+S|fx2C0$k}jpCgc(3-P(Y+ZBm@)~8fFLqk(3amq=Z30>5>o-kPhjgOFD<{ zk`V6ZobR0b-T&bJH1qEL?!DJu@vLX9H&jzyiG+}z5Ca2)gHfCVD9Ab zs<(>;Y%yb_`^@#9aSTkbkH@F2*lG1i*_%N-Cp_w_k1#qBo3y&8>7jmp^b`T`&opfL zqQ{)Sx@Iblx$wW#d%05%`kkF%9gXz%Bsragw@EqV3V-K)tfSll@3)4)MN;Rwe{Y{1 zps0;6xZiv$aqD!keTJEl+dl>8(T0@a%dw@k9x8YppaL{HUS_T;p&Z;RCm)&ek<>hr?c ze)*yb%Ln1<>_`+GBfsuTadEJ+OUTzJytN1X8^jFR6%D zUSzb!dc~HbzXsaBex{ea;%@V8>*jYpf0_V&`CD62_t*pynfq?mGGu(bu&nozwenTH zY+S#HU4}I9aMd%$?vS1n4SU_rY=1Z}_U_O}VCc$V>TK%=3rQ(VlJ>`&tuO<3+jxm{W^!3Y(N z5ydDQhw{8OTD!qquK4ftltvtCeOfvs>F?zF{cgS-)a4n)A9%vL1c8SMnkLlFA=3v9(bS7o`*L2c!byOdV7p(qdd-G4P)k$w%O_@kGDdvqtxNPHX zc@Y|XlrbyB#lOD?x7OOx(XqFy%diSQui>*FJKqu##U%0S>~PI)tVqiH3{uo}d(*Do z_v&)=s}YO$;eZhC?c29657MsYa!PR*k&_b>J_pIOW~Dgu^YiB?JM9`U`~+yripTa; z^|L%Ig%DCyXJ=Eg8EyiUi;K%VN%Zvcq`}n0B(ky@r^mJ^H<$LlGrOow6vvZtxoi8l+Wi! z^a}%*kl$?kzCuOS3ZI^u1$(0?7TDRgg_5eeku`x5fiU%($ZA<@G}f0SqCl9y|C0w; zYq?qHB$Kk4$=ZLmQ>la-UOKJhg*vQD~`FVG?CF-WXc$|-ZyjE2| zF`sc&iB(@}jLl;azLV|g^R=9`Ha&wt8dOkF(DNUzYa<0T zqUSZp9@(6T_rHP!QAtTj{{H?kXuX1`^Dke`KRMW%tjzMMQHo+N3IFSb#vg7QpJy+B z3i|jHB?e2N+}zx}&yT<6=Qrc3F8ZNwkTL5%ecHSV`QU8gb85+$e)A>*_(XJolz~D# zY4SD z24+ewj~{o2QQzg|<=y>`tB8f;pFKFJy}9E%J~-9ikD(E)RIBH|7i|HBLSMXxQ9%{l z8(=8ioC5=;_5@kEU~Uq+!$v-Qt=qIhsyKgg6Aj(ti@e3yoLz!^)x-~U8gn}UKDawWrO zgXgWxU50ELt=Xw(prRVCup48Rdc8SO@gy?CR)y%W`6D`%lIwDj<;t&Vr8h;Qshm-1 zB<|KKP;-t(+IQneL@J0cNt}&Oy}j#}c?~tgh)|Fh`d05xz08CKAVY!lDEJcRqP&P2 zw@q*dP&UHgM)g9d;C0O+z=7Nae=V@5OZfd9X_poM4e@T*4_LdiU8+{fC!(%%lKoQHY zQzGdC|9Nqn`|e!{#6l}q+MAfPG~cyB4r5ieOlcqJY!$-%0FBbgRwdZzX$_?mvmfst zAAgr7OO`3E4)q05zSth66u?g>0_&Nn^V%EY_8)G~Km){*-0!&atPrZLqq7%l<2T%% zf}Wb1deIX6BCQMPk;tieH~Q@N;MCxt@T<*d_Jf>vlp+fY3tJh4MKh)AAqrgK(EPcI zF_V_R1|lIc9=m4Efdu1U27g2Py+AK;)LE|bv+^W2j#vq!zA?@aw|6gNIWt&_RnFMiJ0agyonO-0yEL2Y)5-pr|TmHEg@v>^jnDS zsr$8T<_hbJPee*=hBylcI1BWP+meMOGo`n$#h)9|H~lG0ohatS%s8;eNlca(C4p+J zob|Cg@80U~*YpUFjLaTDgh0I|Z4-W5&Vv~au%u?Pa4_wHTumoIPe8wUPm93QBlN$s;Wnl>~V-PqVjL?bYV zrNVlI$gfUMZ|>_WE-E6WxrhR%d!c)h#8n8c&8eiQIFZUTUGFWy&fdB%l@bm_R6^+& z8PkcwKTqqpx`QjeXvSg8!qjGoLYlr_%L)v{VdU_mJ034Jw~>tacx~nM^mIQzzl4Na z#%2+~;vj$Nh~1u1gm%7VHVH~IXtqGlfx7cV$YN| zfn2RB$#ivfHESt(oJ_l0^(6Xd1P2bc2cb7NHvy)_o^ZS4CrXhqh!VfXH8zt}_TIF^ zt1)j2A6O!ulJYtZXAtZCS^1uU^{BM?G$Ts-Y@W>bT}Cff$`z0fF>DrnMmzTA>hcfy z9YyQkN|k~Q7sBl9OUGN2RgKEmbDxru?tAWAFuFw;l)>oUTt%bI4GvNy+~;E57g`C7 z=jaEcSYG+IAA%@OfaYXnNgecvi09)L*Y2z6>QY70<7u;$A)RJwPj_3$(qw#@X{_bc z)FK)hu0{&<5&rjBkM0%FX?t9rt{`+J78VyfB|>O%v{{m`iNc{Y^q?(*;q48w(Y#7H z`Ss|m$6|!%0eU`nG&3hh=4e<)+!jOGo6owpudmYB!Pb^u96q<){gMCeYxvHLRAQVj z*f>FsfeW{WM4N>Oxzd;BRDA|~w`NPJ1tgC@b2nsuA5S_hcL0><>ypCGi^4_Ky*|p% z&yPtcHfzGxsU3Lp4VE{o{9+KuAxMdrf6h}1su->}RpX4k_?QFIR#WD8 zm+E>(86V@zvx5}(8UHqq3rR|CmODsqZ|@ghzJDL>>(i)jh1uC{fPg46t{%+$PTKS7 zzRO};M9EEh`iTsssN(uF!ZEmDrnDl&ePfhgP|)dgfBDHr0o)A8>gUU~z7-`Q)=!75U(CC4fAV)PWM#+$xJNx`O!@Y5%rN;r_W*H1cNJe&}T~1EU_q5v-?rjrv$eY{x>0CQ(z zg-?A&e%+-K9TOAdwlONV(f8^8P&&Eq)%mQ}5=bMRot?Y9YHDg#xD#LEhzgrqCxjOW z)tA7MG#D+NoXgPA5b%yjoq-T=A}0@z4G5qr9s%=RV`Xp0LF4DonW(AVPWLUR-u$@_ zkQGp2#9QOW-__A)Q(x$gySw7=+S%D53Z@^Kn9O{4n1q*o3J3#Bf7k_IlFXblPYF`c z(a{x|3MM7|JxXqlK-_c8<9ZhwirD*TX6Vdzv{pbzY9^%KrIAXfEuGcSXiG*K5FGwF! z3YgS#r&TWB{Cl))VCIs7C(`&_Ew~7r|G+j_aqbnJ-_nxBlqPk5JQ2v;PjR1v%LKyo z{`-+QhmMor!-o%V@ksXf_X&xJjLKkY=H?leLF?=5Yzo286dC1h#>N#7jFCuWdPc^V zj`nt|(L#e>@9OM0qP#vhlFiE6+tc&@<_8dUKi|^`8huw42x1a*Ti1>4Je;OveE9cr z3QEdrAOO1nV3fI-C)1GJdzfRDufRj2oi-1-PDv|zCP9_e4Gj&o4;-hebaK=O(tjxm z0Jo0!?ZO$-)G5kl&H{G}9%5u>PK}PnNHa7xg8?h(p)xUqODU2u z^!@u}Nl$0Mp<`oWG;s|fmAM_bq>~Nj(>sb0^g#=@n!r3(4^at>4_xtoNK<7hmvKCG z!32C0xN2wy?_bm6C?Vy%f`VB67km?dalhZcl7A0lsFu9gZJ`k`j!t;;*_P#IKIRGTw( z4F9@e*~J_TH9oHFW3a;m zGL-w-q0Q985l!Gb8m1^FwB0bw%4!kzgXuRbCp-IG+kJd~u*7wY*#6fy4naXOvC#+! z1On2owR3?3FOaobL8Jsl?D);d??ig@V^|pB6sOfaA0Vul4PI}m6p>os9YV*NR9P_w z7M9u3(O{!Rdc3}%;T36cn9=FhuQIEn^^skhDv!O}y$TX$>PZO+c;;FRi&QipoPp^@ z%)nNaws-*PDHs`zjg4zTQEDN=V2|~7(sBAn(;b15;PR%pxVXYXzO4IMXj!FvVH$$i z&On7xI0w*Z4OHfIDRz*$O;6j@bZ2KLDg%OzL@3)SdyksN6y^{Bi~#Jm5-~s=)7;$r z;f~S*Hwu)eKYl!5Wu2S7x)c*AdF1N#H@gHt>78dN0gT&oP7T*g5ZRA2Op;BT;N_V z$v6dkqrSd=-6<;Tp{=d$HG|P6l|ah;In+s1na^E(sHAlF&svnp_`t<_K{4mui=$CC zK>ebVl0w0rk?7rBcp_vmcTp0sAm8)JS$Lc??SQeC7C9|#Zl`x#dlZW=x`z(PqX^jn z#nUynU8e%DVrgk9u5v%z+N9>y*X8@BoOT^qLY$R*QXiwD$aJF<_BeYwpbk#$njNUN z&P0C?Ev%)g%F)oz$3V-L)#mGd zZJBu$9z3O5h!h1f522SD?eOA~&VW1%e5VFp7Ev7amk zY7T$o_7j{^ogz*rlCp7ma$(7(?#OjCzoim=r0WdgZEnu28~>MUpmtDCPY*89t&Ref z5sS^ILio-STcTCj+2|EO5&%Jk`~Q2^!AhUZ*=j}tc}*v`p7P_zp^=dZ`tkXl<6q5g z>e}MfG@OkT1Ih>YC;Ue`-#;6st4o5Dovz&a@4~`V)YRhtSuku}@4Vbq>v>=X3nChp zTQ9p*v5<-fQQW9JDg5#d9YGeMkY%7}5YW0>n!s4ve-73sfMAe4wtWsR^=Y8HiusxEy}AsM9FhKBU?^kEsQjqlKHZHlI* zvy+uhi&#qHUdM&YG!uMH?_g#Nt)YW`1nMnIbQ2Vt*1_fngIP}y!{pMx=iZ(ktSoX< zmSj|eGlYctLwBYuMM3Jd|0T?5iS-KNo_MwY!>W(DTiM5_emURa<;$1GEJX~IATTX0 zK@Eixepy4QXgUOnJi|+|_BFx12n<`v!9pe5OzApEi?Rg3wEgAoL0iD3i%set=&#`% z4ua-Tyguku)j7>d+t4lOnyTVw!SW1phLycv+u%`9P}~wQ=H_pC*YnCsPhVfmtPvy8 zEri5KHX%=iVEu1-!}t^IOgw~?_MIbx*&_=nELJ}Bjhh=^V&$+9#y{$#w4^5fM31!4ZJ8_FcN)jw6_;c2HGJ=gf# zY`tkRg1%zkfX9H2W#bBk_O}AM+E;6XxT-75%S~Lr=bGNm%*^cay<5yL$K;W{9b@wh zqaBoO!-CNN?9%6tedEoWH^Ibo!l1whF>K{M7JJ0FP*7YSkU?qzh0it0y~TUXVhKgMxVW4j zu3#(u5HddOl^=gucm7$?w zB$Gt2W2WU>0EzR_SX}C#l-1RP^$`ocjRFA_x}j&o?4OjQhjZS;*0M*}*4D;~P20bY z{-1*2%xo9uvG;^sQBhH3SlEv@r^1eTmyD7g{YQ4azkdD(XF^7aYs{9r;wvgDs{FTa zgK}^3yF-=1gxsTmOlf|of`|4dRG5FrXBa$5*MSUQM1B78KMc_mF^dsb&3b=9v z%tLAl2m5p9&s2_8Fu(;V2q*$F$d~c{_hafX@qvFwM+fjW(A@yN1d0)z{T^^~vXrI}V_=g#P~heP`wR z!b1DKg`eFC{IOwX_jz~-EELx6f>uMf>E*W}k&!RuG@oP-a5xAF2{~008>?VWPCM)C zww@JLRpYa>riRRt9&kOq_;O9)xDqc+lhsyAge7(@n(2~X~G~jXmGUAQ^wn$9&u1kU7B$+>J8Uq6Z z;`vxvb#pjKF8uCTE)DRl#PNlRNdNG#>r%&usw(l0YOj-RxTGTW=Zxfg=8E3lQeg#h zHiF{*-wB#mybf0ffR?nGj-f`*tNm=N%Y1i$V+e&+gN$scDRnJIxf;Cp(|y4FoIfE@ zgDfAH^=S1^HpVne_<4AeWPC5HT~?OfWpHoda`lVKt;uk|+eyu)&jPkd9j<+I>MXvR z`oN8FUyzqKV1TeEKI^u}qRjEQxpO6&@p^ZNMnA<0V zl@%j9e&6OPXcU~){Yn8!=0^zB+*w5LKD^rmu&AC7PJP3hM=|I=$``*C?5d$ zI|>?z2qVddE-nWX6?O>9=Wo-&>qr~Wx@Y(G*rfuTMgn|(a!mi*zeP|OwXa^i5_SAA z>ECAzP{Q%kt^OF`;7?GE3H(N&K84pzDJv)_sH&>k+1+(aQq=qaX3bHJq9Rppj+is( z5+dX3nsoZ`G%)N12>*Gntliw4e{vpIe@0t-du2t%{OGD*6D9l6T6%TMoB2wB+O0o- zHZfc+{Cr>4Ie|IU4m$g^uO?_?R{cL*zm{cyca{WN&+N&+J~IvC>1b<%GB++eYP-n~ z+_KOvSnXqc{Cut-o-E_4g4_mjek)_6LIL*-bD=|{!3~H|@Wj@c0p>4(Y56o~I1D-O zCpi5|UY7iG&$_ajmPm=?^n;_ZP%00`=q%S?FO85!Mn>R^2jH0@L^y5O5P!Zbr*?ar zZK2t2j8s19A-o$n{+tZ6k-r9r%PT6Hw4O8OkPzi!%6)HODl0a?BHb+cdNSjs(UU`h z^$vrH_2+sb(U+%XII11efJ;^4;9)$BJ}~EC9OC;-_3t;YxhssbSs|KJn`}++Q5xg% MBlU+x@@4`517ky`SpWb4 literal 8199 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 Date: Wed, 6 Mar 2024 07:55:53 +0100 Subject: [PATCH 04/16] test: testStateMachineVsStateMachineModelConsistency() update javadoc image --- .../statemachine/uml/UmlStateMachineModelFactoryTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java index 1c38cdbca..bfebe9c1c 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java @@ -171,8 +171,8 @@ public void testSimpleRootRegions() { /** * Test {@link StateMachine} vs {@link StateMachineModel} consistency.
- * In this (failing) test, one can notice that the statemachine instance has a duplicated transition "S3->S4" as illustrated here
- * some + * In this (failing) test, one can notice that the statemachine instance has a duplicated transition "S1->S2" as illustrated here
+ * */ @Disabled @Test From 138146893f390d257c51e94c84666a2235ab7d59 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Fri, 5 Jul 2024 10:40:31 +0200 Subject: [PATCH 05/16] feat: add PlantUml rendering feature --- .../spring-statemachine-uml.gradle | 13 + .../plantuml/ContextTransition.java | 27 + .../statemachine/plantuml/PlantUmlWriter.java | 786 ++++++++++++++++++ .../plantuml/PlantUmlWriterParameters.java | 206 +++++ .../plantuml/StateMachineHelper.java | 115 +++ .../plantuml/helper/NameGetter.java | 142 ++++ .../plantuml/helper/RegionComparator.java | 43 + .../plantuml/helper/StateComparator.java | 12 + .../plantuml/helper/TransactionHelper.java | 142 ++++ .../plantuml/helper/TransitionComparator.java | 13 + .../plantuml/PlantUmlWriterTest.java | 320 +++++++ .../uml/action-with-transition-choice.png | Bin 0 -> 23391 bytes .../uml/action-with-transition-choice.puml | 30 + .../uml/action-with-transition-junction.png | Bin 0 -> 24147 bytes .../uml/action-with-transition-junction.puml | 30 + .../uml/broken-model-shadowentries.png | Bin 0 -> 2387 bytes .../uml/broken-model-shadowentries.puml | 14 + .../statemachine/uml/initial-actions.png | Bin 0 -> 4106 bytes .../statemachine/uml/initial-actions.puml | 14 + .../statemachine/uml/missingname-choice.png | Bin 0 -> 9640 bytes .../statemachine/uml/missingname-choice.puml | 22 + .../statemachine/uml/multijoin-forkjoin.png | Bin 0 -> 26848 bytes .../statemachine/uml/multijoin-forkjoin.puml | 45 + .../uml/pseudostate-in-submachine.png | Bin 0 -> 6446 bytes .../uml/pseudostate-in-submachine.puml | 23 + .../uml/pseudostate-in-submachineref.png | Bin 0 -> 6446 bytes .../uml/pseudostate-in-submachineref.puml | 23 + .../statemachine/uml/simple-actions.png | Bin 0 -> 8171 bytes .../statemachine/uml/simple-actions.puml | 17 + .../statemachine/uml/simple-choice.png | Bin 0 -> 9647 bytes .../statemachine/uml/simple-choice.puml | 22 + .../uml/simple-connectionpointref.png | Bin 0 -> 25783 bytes .../uml/simple-connectionpointref.puml | 30 + .../statemachine/uml/simple-entryexit.png | Bin 0 -> 16653 bytes .../statemachine/uml/simple-entryexit.puml | 29 + .../statemachine/uml/simple-eventdefer.png | Bin 0 -> 4494 bytes .../statemachine/uml/simple-eventdefer.puml | 17 + .../statemachine/uml/simple-flat-end.png | Bin 0 -> 5238 bytes .../statemachine/uml/simple-flat-end.puml | 18 + ...simple-flat-multiple-to-end-viachoices.png | Bin 0 -> 9039 bytes ...imple-flat-multiple-to-end-viachoices.puml | 25 + .../uml/simple-flat-multiple-to-end.png | Bin 0 -> 5612 bytes .../uml/simple-flat-multiple-to-end.puml | 20 + .../statemachine/uml/simple-flat.png | Bin 0 -> 4001 bytes .../statemachine/uml/simple-flat.puml | 15 + .../statemachine/uml/simple-forkjoin.png | Bin 0 -> 19517 bytes .../statemachine/uml/simple-forkjoin.puml | 43 + .../statemachine/uml/simple-guards.png | Bin 0 -> 9185 bytes .../statemachine/uml/simple-guards.puml | 18 + .../statemachine/uml/simple-history-deep.png | Bin 0 -> 19209 bytes .../statemachine/uml/simple-history-deep.puml | 30 + .../uml/simple-history-default.png | Bin 0 -> 13406 bytes .../uml/simple-history-default.puml | 27 + .../uml/simple-history-shallow.png | Bin 0 -> 12638 bytes .../uml/simple-history-shallow.puml | 24 + .../statemachine/uml/simple-junction.png | Bin 0 -> 16057 bytes .../statemachine/uml/simple-junction.puml | 30 + .../uml/simple-localtransition.png | Bin 0 -> 26714 bytes .../uml/simple-localtransition.puml | 28 + .../statemachine/uml/simple-root-regions.png | Bin 7946 -> 16497 bytes .../statemachine/uml/simple-root-regions.puml | 26 + .../statemachine/uml/simple-spels.png | Bin 0 -> 9732 bytes .../statemachine/uml/simple-spels.puml | 16 + .../statemachine/uml/simple-state-actions.png | Bin 0 -> 6161 bytes .../uml/simple-state-actions.puml | 18 + .../statemachine/uml/simple-submachine.png | Bin 0 -> 5017 bytes .../statemachine/uml/simple-submachine.puml | 21 + .../statemachine/uml/simple-submachineref.png | Bin 0 -> 10359 bytes .../uml/simple-submachineref.puml | 30 + .../statemachine/uml/simple-timers.png | Bin 0 -> 11195 bytes .../statemachine/uml/simple-timers.puml | 22 + .../uml/simple-transitiontypes.png | Bin 0 -> 5576 bytes .../uml/simple-transitiontypes.puml | 16 + .../uml/transition-effect-spel.png | Bin 0 -> 6073 bytes .../uml/transition-effect-spel.puml | 14 + 75 files changed, 2576 insertions(+) create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java create mode 100644 spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java create mode 100644 spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.png create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.puml diff --git a/spring-statemachine-uml/spring-statemachine-uml.gradle b/spring-statemachine-uml/spring-statemachine-uml.gradle index da00a62b4..268a1c070 100644 --- a/spring-statemachine-uml/spring-statemachine-uml.gradle +++ b/spring-statemachine-uml/spring-statemachine-uml.gradle @@ -27,11 +27,24 @@ dependencies { api 'org.eclipse.emf:org.eclipse.emf.ecore.xmi' api 'org.eclipse.emf:org.eclipse.emf.ecore' api 'org.eclipse.emf:org.eclipse.emf.common' + + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation'net.sourceforge.plantuml:plantuml-lgpl:1.2023.13' + implementation 'jakarta.validation:jakarta.validation-api:3.1.0' + testImplementation(testFixtures(project(":spring-statemachine-core"))) testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.springframework:spring-test' testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.jupiter:junit-jupiter-params' testImplementation 'org.awaitility:awaitility' + + // lombok + implementation 'org.projectlombok:lombok:1.18.32' + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + testImplementation 'org.projectlombok:lombok:1.18.32' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java new file mode 100644 index 000000000..4c48cc6dc --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java @@ -0,0 +1,27 @@ +package org.springframework.statemachine.plantuml; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.lang.Nullable; +import org.springframework.statemachine.StateContext; + +@AllArgsConstructor +@Getter +public class ContextTransition { + final S source; + final E event; + final S target; + + public static ContextTransition of(@Nullable StateContext stateContext) { + if (stateContext != null) { + return new ContextTransition<>( + stateContext.getSource().getId(), + stateContext.getEvent(), + stateContext.getTarget() != null + ? stateContext.getTarget().getId() + : null + ); + } + return null; + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java new file mode 100644 index 000000000..f000c05e2 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -0,0 +1,786 @@ +package org.springframework.statemachine.plantuml; + +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import net.sourceforge.plantuml.SourceStringReader; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.lang.Nullable; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.plantuml.helper.*; +import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.*; +import org.springframework.statemachine.support.StateMachineUtils; +import org.springframework.statemachine.transition.Transition; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toMap; + +/** + * This class convert a Spring StateMachine to a PlantUml StateDiagram
+ *
+ * To display *.puml diagram, install PlantUml plugin
+ * https://plugins.jetbrains.com/plugin/7017-plantuml-integration
+ *
+ * For PNG support, install GraphViz
+ * https://graphviz.org/download/#windows
+ *
+ * Install then define GRAPHVIZ_DOT environment variable:
+ *
+ * GRAPHVIZ_DOT=C:\Program Files\Graphviz\bin\dot.exe
+ *
+ * To obtain better results, {@link Action} s and {@link Guard} s should: + *
    + *
  • be {@link org.springframework.context.annotation.Bean} s
  • + *
  • implement {@link BeanNameAware}
  • + *
  • have a "String beanName" field (can be private)
  • + *
+ *
+ * Links:
+ * + */ +// Disabling "Method has * parameters, which is greater than 7 authorized." warning +@SuppressWarnings("squid:S107") +public class PlantUmlWriter { + + private static final Log log = LogFactory.getLog(PlantUmlWriter.class); + + private static final String INDENT_INCREMENT = " "; + + // History states are handled in a special way: + // 1 - During the 1st pass, History states 'IDs' are 'computed' and collected in 'historyStatesToHistoryId' + // 2 - During the 2nd pass (the main one), 'history' transitions are collected in 'historyTransitions' + // 3 - Then, the collected 'historyTransitions' are added at the end of the PlantUml diagram using 'historyStatesToHistoryId' + + private Map, String> historyStatesToHistoryId; + private List> historyTransitions; + + // Comparators. Used to keep order of region, states and transitions stable in generated puml + + private final StateComparator stateComparator = new StateComparator<>(); + + private final Comparator> transitionComparator = new TransitionComparator<>(); + + private final Comparator> regionComparator = new RegionComparator<>(stateComparator); + + // + + public void save( + @NotNull StateMachine stateMachine, + @NotNull File file + ) throws IOException { + save(stateMachine, null, null, file); + } + + /** + * @param stateMachine stateMachine + * @param stateContext stateContext + * @param plantUmlWriterParameters plantUmlWriterParameters + * @param file filename must be "*.puml" or "*.png" + * @throws IOException + */ + public void save( + @NotNull StateMachine stateMachine, + @Nullable StateContext stateContext, + @Nullable PlantUmlWriterParameters plantUmlWriterParameters, + @NotNull File file + ) throws IOException { + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + } + String plantUmlDiagram = toPlantUml( + stateMachine, + stateContext, + plantUmlWriterParameters + ); + + if (file.getName().endsWith(".puml")) { + Files.write(file.toPath(), plantUmlDiagram.getBytes()); + } else if (file.getName().endsWith(".png")) { + SourceStringReader reader = new SourceStringReader(plantUmlDiagram); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + reader.outputImage(os); + Files.write(file.toPath(), os.toByteArray()); + } else { + throw new IllegalArgumentException("file name must be *.puml or *.png"); + } + } + + public String toPlantUml(StateMachine stateMachine) { + return toPlantUml(stateMachine, null, null); + } + + /** + * Convert a State machine in PlantUML notation
+ * limited support! + * + * @param stateMachine stateMachine + * @param stateContext stateContext + * @param plantUmlWriterParameters plantUmlWriterParameters + * @return plantUml representation of the stateMachine + */ + public String toPlantUml( + StateMachine stateMachine, + @Nullable StateContext stateContext, + @Nullable PlantUmlWriterParameters plantUmlWriterParameters + ) { + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + } + + // 1st pass: Collecting history states + historyStatesToHistoryId = StateMachineHelper.collectHistoryStates(stateMachine); + historyTransitions = new ArrayList<>(); + + StringBuilder sb = new StringBuilder(); + sb.append(""" + @startuml + 'https://plantuml.com/state-diagram + + 'hide description area for state without description + hide empty description + + """); + + // 2nd pass: processing statemachine AND collecting history transitions in 'historyTransitions + processRegion(stateMachine, stateContext, plantUmlWriterParameters, sb, "", null); + + // finally, adding the collected history transitions + for (Transition transition : historyTransitions) { + processPseudoStatesTransition( + ContextTransition.of(stateContext), + transition, + transition.getSource(), + transition.getTarget(), + this::getHistoryStateId, + plantUmlWriterParameters, + sb, + "" + ); + } + + sb.append("\n@enduml"); + + log.debug("toPlantUml:" + sb); + + return sb.toString(); + } + + // TODO check this... is this a good idea? + private interface HistoryIdGetter { + String getId(State state); + } + + String getHistoryStateId(State state) { + if (historyStatesToHistoryId.containsKey(state)) { + return historyStatesToHistoryId.get(state); + } else { + return state.getId().toString(); + } + } + + private void processRegion( + @NotNull Region region, + @Nullable StateContext stateContext, + @Nullable PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent, + @Nullable Predicate> transitionAllowed // not working for the moment + ) { + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + } + + // check 'entry' and 'exit' pseudo states: + // these states MUST NOT be added in this region, BUT in the subregion / submachine they are related to! + Map>> allStates = region.getStates() + .stream() + .collect(Collectors.groupingBy(this::isEntryOrExit)); + + List> entryAndExitStates = allStates.get(Boolean.TRUE); + List> otherStates = allStates.get(Boolean.FALSE); + + // states + processStates( + region, + stateContext, + entryAndExitStates, + otherStates, + plantUmlWriterParameters, + sb, + indent + ); + + // transitions + ContextTransition currentContextTransition = ContextTransition.of(stateContext); + processPseudoStatesTransitions(region, currentContextTransition, plantUmlWriterParameters, sb, indent); + processTransitions(region, plantUmlWriterParameters, currentContextTransition, sb, indent, transitionAllowed); + } + + private Boolean isEntryOrExit(State state) { + if (state.getPseudoState() == null) { + return Boolean.FALSE; + } + return PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()) + || PseudoStateKind.EXIT.equals(state.getPseudoState().getKind()); + } + + // TODO create processState() with optional '{' and '}' to allow text on stereotype states? + // warning -> this change the visual representation of the state, using the 'default' representation :/ + private void processStates( + Region region, + StateContext stateContext, + @Nullable Collection> entryAndExitStates, + List> otherStates, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + S currentState = region.getState() != null + ? region.getState().getId() + : null; + + Collection> regionTransitions = region.getTransitions(); + + // associating each entry to its targets, and each exit to its sources (as per transitions) + Map>> allStates = entryAndExitStates == null ? Map.of() + : entryAndExitStates + .stream() + .collect(Collectors.groupingBy(state -> PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()))); + + List> entryStates = allStates.get(Boolean.TRUE); + Map, List> entryToTargetStates = entryStates == null ? Map.of() : + entryStates.stream().collect( + toMap( + entry -> entry, + entry -> getEntryTargets(entry, regionTransitions), + (s, s2) -> { + throw new IllegalStateException("This should not happen!"); + })); + + List> exitStates = allStates.get(Boolean.FALSE); + Map, List> exitToSourceStates = exitStates == null ? Map.of() : + exitStates.stream().collect( + toMap( + exit -> exit, + exit -> getExitSources(exit, regionTransitions), + (s, s2) -> { + throw new IllegalStateException("This should not happen!"); + })); + + // processing states + otherStates.stream() + .sorted(stateComparator) + .toList() + .forEach(state -> processState(state, currentState, entryToTargetStates, exitToSourceStates, stateContext, plantUmlWriterParameters, sb, indent)); + + sb.append("\n"); + } + + private List getEntryTargets(State entry, Collection> regionTransitions) { + return regionTransitions.stream() + .filter(aTransition -> entry.getId().equals(aTransition.getSource().getId())) + .map(aTransition -> aTransition.getTarget().getId()) + .toList(); + } + + private List getExitSources(State exit, Collection> regionTransitions) { + return regionTransitions.stream() + .filter(aTransition -> exit.getId().equals(aTransition.getTarget().getId())) + .map(aTransition -> aTransition.getSource().getId()) + .toList(); + } + + private void processState( + State state, + S currentState, + Map, List> entryToTargetStates, + Map, List> exitToSourceStates, + StateContext stateContext, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + if (state.isSimple()) { + processSimpleState(state, currentState, plantUmlWriterParameters, sb, indent); + } else if (state.isSubmachineState()) { + if (state instanceof AbstractState abstractState) { + processSubmachine(abstractState, currentState, plantUmlWriterParameters, sb, indent, stateContext, entryToTargetStates, exitToSourceStates); + } + } else if (state.isComposite() || state.isOrthogonal()) { + if (state instanceof RegionState regionState) { + processCompositeOrOrthogonalState(regionState, currentState, plantUmlWriterParameters, sb, indent, stateContext, entryToTargetStates, exitToSourceStates); + } + } else { + throw new NotImplementedException("Unexpected state type " + state.getId()); + } + processStateDescription(state, sb, indent); + } + + private void processCompositeOrOrthogonalState( + RegionState regionState, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent, + StateContext stateContext, + Map, List> entryToTargetStates, + Map, List> exitToSourceStates + ) { + sb.append(""" + %s { + """ + .formatted(stateToString(indent, regionState.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + + final String regionIndent = indent + INDENT_INCREMENT; + List> regions = regionState.getRegions().stream() + .sorted(regionComparator) + .toList(); + + for (int i = 0, nRegions = regions.size(); i < nRegions; i++) { + Region subRegion = regions.get(i); + processRegion(subRegion, stateContext, plantUmlWriterParameters, sb, regionIndent, new Predicate>() { + @Override + public boolean test(Transition transition) { + // TODO implement this by checking if source or target state belong to another region ??? +// log.warn("Transition {} from / to another region is not allowed"); + return true; + } + }); + processEntries(currentState, subRegion.getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); + processExits(currentState, subRegion.getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); + // using "--" caused problem ... how to solve this..? +/* + if (i != nRegions - 1) { + // Separating regions. see "États concurrents [--, ||]" https://plantuml.com/fr/state-diagram#73b918d90b24a6c6 + sb.append(regionIndent).append("--\n"); + } +*/ + } + sb.append(""" + %s} + """.formatted(indent)); + } + + private void processSubmachine( + AbstractState abstractState, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent, + StateContext stateContext, + Map, List> entryToTargetStates, + Map, List> exitToSourceStates + ) { + sb.append(""" + %s { + """ + .formatted(stateToString(indent, abstractState.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + final String regionIndent = indent + INDENT_INCREMENT; + processRegion(abstractState.getSubmachine(), stateContext, plantUmlWriterParameters, sb, regionIndent, null); + processEntries(currentState, abstractState.getSubmachine().getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); + processExits(currentState, abstractState.getSubmachine().getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); + sb.append(""" + %s} + """.formatted(indent)); + } + + private void processSimpleState( + State state, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + if (state.getPseudoState() != null) { + processPseudoState(state, currentState, plantUmlWriterParameters, sb, indent); + } else { +// TODO allow plantUmlWriterParameters to add links in description ?? +// eg: state "CarWithWheel [[http://plantuml.com/state-diagram]]" as CarWithWheel + sb.append(""" + %s + """ + .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + } + } + + private void processEntries( + S currentState, + Collection> regionStates, + Map, List> entryToTargetStates, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + entryToTargetStates.keySet() + .forEach(entryState -> entryToTargetStates.get(entryState) + .stream() + .filter(targetStateId -> regionStates.stream().anyMatch(regionState -> targetStateId.equals(regionState.getId()))) + .forEach(s -> processPseudoState(entryState, currentState, plantUmlWriterParameters, sb, indent))); + } + + private void processExits( + S currentState, + Collection> regionStates, + Map, List> exitToSourceStates, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + exitToSourceStates.keySet() + .forEach(exitState -> exitToSourceStates.get(exitState) + .stream() + .filter(sourceStateId -> regionStates.stream().anyMatch(regionState -> sourceStateId.equals(regionState.getId()))) + .forEach(s -> processPseudoState(exitState, currentState, plantUmlWriterParameters, sb, indent))); + } + + private void processPseudoState( + State state, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + PseudoStateKind pseudoStateKind = state.getPseudoState().getKind(); + switch (pseudoStateKind) { + case INITIAL -> { + if (StateMachineUtils.isPseudoState(state, PseudoStateKind.INITIAL)) { + sb.append(""" + %s + """ + .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + } + } + case HISTORY_SHALLOW, HISTORY_DEEP -> { + // no-op + // History states are NOT added in the diagram as per themselves + // They are added 'just' by creating a transition involving an history state, i.e., [H] or [H*] + // see historyStatesToHistoryId and historyTransitions + } + case ENTRY, EXIT -> sb.append(""" + %sstate %s <<%s>> + """ + .formatted( + indent, + state.getId(), + getPseudoStatePlantUmlStereotype(pseudoStateKind) + ).stripIndent()); + case END, CHOICE, FORK, JOIN, JUNCTION -> sb.append(""" + %s'%s <<%s>> + %sstate %s <<%s>> + %snote left of %s %s: %s + """ + .formatted( + indent, + state.getId(), + pseudoStateKind.name(), + indent, + state.getId(), + getPseudoStatePlantUmlStereotype(pseudoStateKind), + indent, + state.getId(), + plantUmlWriterParameters.getStateColor(state.getId(), currentState), + state.getId() + ).stripIndent()); + } + } + + /** + * Return a PlantUML stereotype for a given PseudoStateKind
+ * we use <> stereotype for JUNCTION
+ * see UML 2 - State Machine Diagram + * + * @param pseudoStateKind pseudoStateKind + * @return PlantUml stereotype corresponding to pseudoStateKind + */ + private String getPseudoStatePlantUmlStereotype(PseudoStateKind pseudoStateKind) { + return switch (pseudoStateKind) { + case INITIAL, JUNCTION -> "start"; + case ENTRY, EXIT -> pseudoStateKind.name().toLowerCase() + "Point"; + default -> pseudoStateKind.name().toLowerCase(); + }; + } + + private String stateToString( + String indent, S state, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters + ) { + return "%sstate %s %s" + .formatted( + indent, + state, + plantUmlWriterParameters.getStateColor(state, currentState) + ); + } + + private void processStateDescription( + State state, + StringBuilder sb, + String indent + ) { + for (E deferredEvent : state.getDeferredEvents()) { + sb.append(""" + %s%s : %s /defer + """ + .formatted( + indent, + state.getId(), + deferredEvent + ) + .stripIndent() + ); + } + for (Function, Mono> entryAction : state.getEntryActions()) { + sb.append(""" + %s%s : /entry %s + """ + .formatted( + indent, + state.getId(), + NameGetter.getName(entryAction) + ) + .stripIndent() + ); + } + for (Function, Mono> stateAction : state.getStateActions()) { + sb.append(""" + %s%s : /do %s + """ + .formatted( + indent, + state.getId(), + NameGetter.getName(stateAction) + ) + .stripIndent() + ); + } + for (Function, Mono> exitAction : state.getExitActions()) { + sb.append(""" + %s%s : /exit %s + """ + .formatted( + indent, + state.getId(), + NameGetter.getName(exitAction) + ) + .stripIndent() + ); + } + } + + + private void processPseudoStatesTransitions( + Region region, + @Nullable ContextTransition currentContextTransition, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + List> states = region.getStates().stream() + .sorted(stateComparator) + .toList(); + for (State state : states) { + // collecting transitions for 'pseudoStateKinds' + if (state.getPseudoState() != null) { + PseudoStateKind pseudoStateKind = state.getPseudoState().getKind(); + switch (pseudoStateKind) { + // already taken care of ;-) + case INITIAL, END, CHOICE, JOIN, JUNCTION, ENTRY, EXIT, HISTORY_SHALLOW, HISTORY_DEEP -> { + // already taken care of ;-) + } + case FORK -> { + // TODO why do I have to do this here? try to do it elsewhere? + for (State nextState : ((ForkPseudoState) state.getPseudoState()).getForks()) { + processPseudoStatesTransition( + currentContextTransition, + null, + state, + nextState, + null, + plantUmlWriterParameters, + sb, + indent + ); + } + } + } + } + } + sb.append("\n"); + } + + private void processPseudoStatesTransition( + @Nullable ContextTransition currentContextTransition, + @Nullable Transition transition, + State sourceState, + State targetState, + @Nullable HistoryIdGetter historyIdGetter, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + S source = sourceState.getId(); + S target = targetState.getId(); + sb.append(""" + %s%s + %s%s -%s[%s]-> %s %s + """ + .formatted( + indent, + historyIdGetter == null ? "" : "'" + source + " -> " + target, // if history transition, add a comment with 'real' state names + indent, + historyIdGetter == null ? sourceState.getId() : historyIdGetter.getId(sourceState), + plantUmlWriterParameters.getDirection(source, target), + plantUmlWriterParameters.getArrowColor( + currentContextTransition != null + && ( + currentContextTransition.getSource() == source + // && currentContextTransition.event == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target + ) + ), + historyIdGetter == null ? targetState.getId() : historyIdGetter.getId(targetState), + TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) + ) + .stripIndent() + ); + } + + private void processTransitions( + Region region, + PlantUmlWriterParameters plantUmlWriterParameters, + @Nullable ContextTransition currentContextTransition, + StringBuilder sb, + String indent, + @Nullable Predicate> transitionAllowed + ) { + + // initial transition + Transition initialTransition = TransactionHelper.getInitialTransition(region); + if (initialTransition != null) { + addTransition( + sb, + indent, + "[*]", // transition from initial state + null, + initialTransition.getTarget().getId(), + plantUmlWriterParameters, + plantUmlWriterParameters.getArrowColor(false), + initialTransition + ); + } + + // region's transitions + for (Transition transition : + region.getTransitions().stream().sorted(transitionComparator).toList() + ) { + // in orthogonalRegion, we do NOT allow transition to states from another region! + if (transitionAllowed != null && !transitionAllowed.test(transition)) { + return; + } + + // collect history transitions: they will be added to the diagram later on + if (isHistoryTransition(transition)) { + historyTransitions.add(transition); + } else { + S source = transition.getSource().getId(); + S target = transition.getTarget().getId(); + + switch (transition.getKind()) { + case EXTERNAL, INTERNAL, LOCAL -> { + String arrowColor = plantUmlWriterParameters.getArrowColor( + currentContextTransition != null + && ( + currentContextTransition.getSource() == source + && currentContextTransition.getEvent() == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target + ) + ); + addTransition(sb, indent, source.toString(), source, target, plantUmlWriterParameters, arrowColor, transition); + } + case INITIAL -> throw new NotImplementedException( + "Unexpected INITIAL transition! They are handled in processInitialTransition(), not here! Check why we reached this exception!" + ); + } + } + } + } + + private void addTransition( + StringBuilder sb, + String indent, + String sourceLabel, + S source, + S target, + PlantUmlWriterParameters plantUmlWriterParameters, + String arrowColor, + Transition transition + ) { + sb.append(""" + %s%s -%s[%s]-> %s %s + """ + .formatted( + indent, + sourceLabel, + source == null ? "" : plantUmlWriterParameters.getDirection(source, target), + arrowColor, + target, + TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) + ) + .stripIndent() + ); + if (StringUtils.isNotBlank(transition.getName())) { + sb.append(""" + %snote on link + %s %s + %send note + """ + .formatted( + indent, + indent, + transition.getName(), + indent + )); + } + } + + private boolean isHistoryTransition(@NotNull Transition transition) { + return isHistoryState(transition.getSource()) || isHistoryState(transition.getTarget()); + } + + private boolean isHistoryState(@NotNull State state) { + if (state.getPseudoState() == null) { + return Boolean.FALSE; + } + return PseudoStateKind.HISTORY_SHALLOW.equals(state.getPseudoState().getKind()) + || PseudoStateKind.HISTORY_DEEP.equals(state.getPseudoState().getKind()); + } + +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java new file mode 100644 index 000000000..22e5a0669 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -0,0 +1,206 @@ +package org.springframework.statemachine.plantuml; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.lang.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This class allows to tweak / fine-tune PlantUml StateDiagram visualisation + * + * @param + */ +public class PlantUmlWriterParameters { + + private static final Log log = LogFactory.getLog(PlantUmlWriterParameters.class); + + // TODO add hidden arrows 'S1 -[hidden]-> S2' + + /** + * Direction of an arrow connecting 2 States + */ + public enum Direction { + UP, + DOWN, + LEFT, + RIGHT + } + + + @Value + @EqualsAndHashCode + private static class Connection { + S source; + S target; + } + + /** + * Map of ( (sourceSate, targetState) -> Direction ) + */ + private final Map, Direction> arrows = new HashMap<>(); + + public PlantUmlWriterParameters arrowDirection(S source, Direction direction, S target) { + arrows.put(new Connection<>(source, target), direction); + return this; + } + + /** + * At least one of source and target must be non-null
+ * See implementation for 'arrow direction rule priority' + * + * @param source source State + * @param target target State + * @return Direction.name() + */ + String getDirection(S source, S target) { + if (source == null && target == null) { + throw new IllegalArgumentException("source and target state cannot both be null!"); + } + Connection sourceAndTarget = new Connection<>(source, target); + if (arrows.containsKey(sourceAndTarget)) { + return arrows.get(sourceAndTarget).name().toLowerCase(); + } + Connection sourceOnly = new Connection<>(source, null); + Connection targetOnly = new Connection<>(null, target); + if (arrows.containsKey(sourceOnly) + && arrows.containsKey(targetOnly) + && !arrows.get(sourceOnly).equals(arrows.get(targetOnly)) + ) { + log.warn( + String.format("Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); + return arrows.get(targetOnly).name().toLowerCase(); + } else if (arrows.containsKey(sourceOnly)) { + return arrows.get(sourceOnly).name().toLowerCase(); + } else if (arrows.containsKey(targetOnly)) { + return arrows.get(targetOnly).name().toLowerCase(); + } else { + return Direction.DOWN.name().toLowerCase(); + } + } + + /** + * Map of ( Connection(sourceSate, targetState) -> Direction ) + * Used to add EXTRA HIDDEN arrows, which are just helping with diagram layout.
+ * This is typically useful to 'force' the position of a state comparing to another, EVEN IF THESE TWO STATES AR NOT CONNECTED in the statemachine :-)
+ *

+ * exemple: + * S1 -left[hidden]-> S2 + */ + private final Map, Direction> hiddenTransitions = new HashMap<>(); + + public PlantUmlWriterParameters hiddenTransition(S source, Direction direction, S target) { + hiddenTransitions.put(new Connection<>(source, target), direction); + return this; + } + + public String getHiddenTransitions() { + return hiddenTransitions.entrySet().stream() + .map(hiddenTransition -> "%s -%s[hidden]-> %s" + .formatted( + hiddenTransition.getKey().getSource(), + hiddenTransition.getValue().name(), + hiddenTransition.getKey().getTarget() + )) + .collect(Collectors.joining("\n")); + } + + @Builder + @Getter + @EqualsAndHashCode + static class LabelDecorator { + String prefix = ""; + String suffix = ""; + + String decorate(String label) { + return prefix + label + suffix; + } + } + + /** + * Map of ( Connection(sourceSate, targetState) -> LabelDecorator(labelPrefix, labelSuffix) ) + */ + private final Map, LabelDecorator> arrowLabelDecorator = new HashMap<>(); + + public PlantUmlWriterParameters arrowLabelDecorator(S source, S target, String prefix, String suffix) { + arrowLabelDecorator.put( + new Connection<>(source, target), + LabelDecorator.builder().prefix(prefix).suffix(suffix).build() + ); + return this; + } + + public String decorateLabel( + @Nullable S source, + @Nullable S target, + @Nullable String transitionLabel + ) { + if (transitionLabel == null) { + return null; + } + + if (source == null && target == null) { + throw new IllegalArgumentException("source and target state cannot both be null!"); + } + Connection sourceAndTarget = new Connection<>(source, target); + if (arrowLabelDecorator.containsKey(sourceAndTarget)) { + return arrowLabelDecorator.get(sourceAndTarget).decorate(transitionLabel); + } + Connection sourceOnly = new Connection<>(source, null); + Connection targetOnly = new Connection<>(null, target); + if (arrowLabelDecorator.containsKey(sourceOnly) + && arrowLabelDecorator.containsKey(targetOnly) + && !arrowLabelDecorator.get(sourceOnly).equals(arrowLabelDecorator.get(targetOnly)) + ) { + log.warn( + String.format("Two 'unary' 'arrowLabelDecorator' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); + return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); + } else if (arrowLabelDecorator.containsKey(sourceOnly)) { + return arrowLabelDecorator.get(sourceOnly).decorate(transitionLabel); + } else if (arrowLabelDecorator.containsKey(targetOnly)) { + return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); + } else { + return transitionLabel; + } + } + + // Colors + + private String defaultStateColor = ""; + private String currentStateColor = "#FFFF77"; + + @Getter + private String transitionLabelSeparator = "\\n"; + + public PlantUmlWriterParameters setTransitionLabelSeparator(String transitionLabelSeparator) { + this.transitionLabelSeparator = transitionLabelSeparator; + return this; + } + + public PlantUmlWriterParameters defaultStateColor(String defaultStateColor) { + this.defaultStateColor = defaultStateColor; + return this; + } + + public PlantUmlWriterParameters currentStateColor(String currentStateColor) { + this.currentStateColor = currentStateColor; + return this; + } + + public String getStateColor(S state, @Nullable S currentState) { + if (state == null) { + log.warn("null state!"); + return ""; // TODO check why this is happening + } + return state.equals(currentState) ? currentStateColor : defaultStateColor; + } + + public String getArrowColor(boolean isCurrentTransaction) { + return isCurrentTransaction ? "#FF0000" : "#000000"; + } + +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java new file mode 100644 index 000000000..5b06b798b --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java @@ -0,0 +1,115 @@ +package org.springframework.statemachine.plantuml; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineBuilder; +import org.springframework.statemachine.config.model.StateMachineModelFactory; +import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.AbstractState; +import org.springframework.statemachine.state.PseudoStateKind; +import org.springframework.statemachine.state.RegionState; +import org.springframework.statemachine.state.State; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StateMachineHelper { + + public static StateMachine buildStateMachine( + StateMachineModelFactory stateMachineModelFactory + ) throws Exception { + StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); + builder.configureModel().withModel().factory(stateMachineModelFactory); + builder.configureConfiguration().withConfiguration(); + return builder.build(); + } + + public static List getCurrentStates(StateMachine stateMachine) { + ArrayList currentState = new ArrayList<>(); + collectCurrentStates(stateMachine, currentState); + return currentState; + } + + private static void collectCurrentStates( + Region region, + ArrayList currentStateAccumulator + ) { + if (region.getState() != null) { + currentStateAccumulator.add(region.getState().getId()); + } + region.getStates().forEach(state -> { + if (state.isSubmachineState()) { + if (state instanceof AbstractState abstractState) { + collectCurrentStates(abstractState.getSubmachine(), currentStateAccumulator); + } + } else if (state.isOrthogonal() || state.isComposite()) { + if (state instanceof RegionState regionState) { + regionState.getRegions().stream() + .toList() + .forEach(subRegion -> collectCurrentStates(subRegion, currentStateAccumulator)); + } + } + }); + } + + public static Map, String> collectHistoryStates(StateMachine stateMachine) { + HashMap, String> historyStatesToHistoryId = new HashMap, String>(); + collectHistoryStates(stateMachine, null, historyStatesToHistoryId); + return historyStatesToHistoryId; + } + + private static void collectHistoryStates( + Region region, + @Nullable State parentState, + Map, String> historyStatesToHistoryId + ) { + region.getStates().forEach(state -> { + if (state.isSimple()) { + collectHistoryState(state, parentState, historyStatesToHistoryId); + } else if (state.isSubmachineState()) { + if (state instanceof AbstractState abstractState) { + collectHistoryStates(abstractState.getSubmachine(), state, historyStatesToHistoryId); + } + } else if (state.isOrthogonal() || state.isComposite()) { + if (state instanceof RegionState regionState) { + regionState.getRegions().stream() + .toList() + .forEach(subRegion -> collectHistoryStates(subRegion, state, historyStatesToHistoryId)); + } + } + }); + } + + private static void collectHistoryState( + State state, + @Nullable State parentState, + Map, String> historyStatesToHistoryId + ) { + if (state.getPseudoState() != null + && ( + state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP + || state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW + ) + ) { + historyStatesToHistoryId.put(state, historyId(parentState, state.getPseudoState().getKind())); + } + } + + private static String historyId( + @Nullable State parentState, + PseudoStateKind pseudoStateKind + ) { + String prefix = parentState == null ? "" : parentState.getId().toString(); + return switch (pseudoStateKind) { + case HISTORY_DEEP -> prefix + "[H*]"; + case HISTORY_SHALLOW -> prefix + "[H]"; + default -> throw new IllegalArgumentException("pseudoStateKind must be an 'history'"); + }; + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java new file mode 100644 index 000000000..bc31a3650 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java @@ -0,0 +1,142 @@ +package org.springframework.statemachine.plantuml.helper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.expression.Expression; +import org.springframework.lang.Nullable; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.action.SpelExpressionAction; +import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.guard.SpelExpressionGuard; + +import java.lang.reflect.Field; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NameGetter { + + private static final Log log = LogFactory.getLog(NameGetter.class); + + /** + * Implement a 'name strategy' based on this sequence: + *

    + *
  • {@link Expression}
  • + *
  • {@link BeanNameAware}
  • + *
  • Lambda's unique "arg$1" parameter
  • + *
+ * If all these strategy are failing, fallback to "class name" of the object + * + * @param object object to get "name" from + * @return name of the object + */ + public static String getName(Object object) { + String name = getSpellExpression(object); + + if (name == null) { + Object uniqueArg = extractArg$1(object, Action.class); + if (uniqueArg != object) { + return getName(uniqueArg); + } + } + + if (name == null) { + Object uniqueArg = extractArg$1(object, Guard.class); + if (uniqueArg != object) { + return getName(uniqueArg); + } + } + if (name == null) { + name = getBeanName(object); + } + + // fallback + if (name == null) { + name = object.getClass().toString(); + name = name.replace("class ", ""); + // remove trailing "/0x..." in class name + // example: "Actions$$Lambda$504/0x0000000800ec3e00" + name = RegExUtils.removeAll(name, "/0x.*"); + // remove trailing "$(0-9)" in action name + // example: "Actions$$Lambda$504" +// name = RegExUtils.removeAll(name, "\\$\\d+"); + + // only keep class name + // a.b.c.D$32/0x25764366 -> D$32 + String nameWithoutDollar = name; + int indexOfDollarSymbol = name.indexOf("$"); + if (indexOfDollarSymbol != -1) { + nameWithoutDollar = name.substring(0, indexOfDollarSymbol); + } + int lastIndexOfDot = nameWithoutDollar.lastIndexOf("."); + if (lastIndexOfDot != -1) { + name = name.substring(lastIndexOfDot + 1, name.length()); + } + } + return name; + } + + @Nullable + private static String getSpellExpression(Object object) { + if (object instanceof SpelExpressionAction || object instanceof SpelExpressionGuard) { + try { + return ((Expression) FieldUtils.readDeclaredField(object, "expression", true)).getExpressionString(); + } catch (IllegalAccessException ex) { + log.error("error while getting SpelExpression", ex); + } + } + return null; + } + + + // Disable "Rename this ... variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'" + // we want to the name of this method to clearly state what is does + @SuppressWarnings({"squid:S100", "squid:S117"}) + private static Object extractArg$1(Object actionWrappedInFunction, Class typeOfArg) { + Field arg$1Field = FieldUtils.getDeclaredField(actionWrappedInFunction.getClass(), "arg$1", true); + if (arg$1Field != null && arg$1Field.getType() == typeOfArg) { + try { + return FieldUtils.readDeclaredField(actionWrappedInFunction, "arg$1", true); + } catch (IllegalAccessException ex) { + log.error("Error while extracting action from function!", ex); + } + } + return actionWrappedInFunction; + } + + @Nullable + private static String getBeanName(Object object) { + Class clazz = object.getClass(); + if (!ClassUtils.getAllInterfaces(clazz).contains(BeanNameAware.class)) { + log.error("Class " + clazz + " doesn't implements " + BeanNameAware.class + "! " + + "Make sure " + clazz + " implements BeanNameAware AND contains a 'String beanName' field." + ); + return null; + } + + try { + // 'clazz' implements BeanNameAware .. let's try to get beanName + Field beanNameFielOfClazz = FieldUtils.getField(clazz, "beanName", true); + if (beanNameFielOfClazz == null) { + log.error("Class " + clazz + " does NOT contains a 'beanNameAware' field! " + + "Make sure " + clazz + " contains a 'String beanName' field." + ); + } else if (beanNameFielOfClazz.getType() == String.class) { + return (String) FieldUtils.readField(object, "beanName", true); + } else { + log.error("Class " + clazz + " contains a '" + beanNameFielOfClazz.getType() + " beanNameAware' field, but type should be 'String'! " + + "Make sure " + clazz + " contains a 'String beanName' field." + ); + } + } catch (IllegalAccessException ex) { + log.error("Error while accessing field!", ex); + } + + return null; + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java new file mode 100644 index 000000000..1bdb42f34 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java @@ -0,0 +1,43 @@ +package org.springframework.statemachine.plantuml.helper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.State; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.IntStream; + +@RequiredArgsConstructor +public class RegionComparator implements Comparator> { + + private static final Log log = LogFactory.getLog(RegionComparator.class); + + private final StateComparator stateComparator; + + @Override + public int compare(Region region1, Region region2) { + List> sourceSortedStates = region1.getStates().stream().sorted(stateComparator).toList(); + List> targetSortedStates = region2.getStates().stream().sorted(stateComparator).toList(); + + List regionComparisonResult = IntStream + .range(0, Math.min(sourceSortedStates.size(), targetSortedStates.size())) + // comparing pairs of states + .mapToObj(i -> sourceSortedStates.get(i).getId().toString().compareTo(targetSortedStates.get(i).getId().toString())) + .toList(); + + // returning first "non 0" comparison result + for (Integer comparisonResult : regionComparisonResult) { + if (comparisonResult != 0) { + return comparisonResult; + } + } + + // this should not happen...? + log.warn("getRegionComparator: unable to compare regions!!"); + return 0; + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java new file mode 100644 index 000000000..964ac28f7 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java @@ -0,0 +1,12 @@ +package org.springframework.statemachine.plantuml.helper; + +import org.springframework.statemachine.state.State; + +import java.util.Comparator; + +public class StateComparator implements Comparator> { + @Override + public int compare(State state1, State state2) { + return state1.getId().toString().compareTo(state2.getId().toString()); + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java new file mode 100644 index 000000000..854bb6627 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java @@ -0,0 +1,142 @@ +package org.springframework.statemachine.plantuml.helper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.lang.Nullable; +import org.springframework.statemachine.plantuml.PlantUmlWriterParameters; +import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.AbstractState; +import org.springframework.statemachine.support.AbstractStateMachine; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.trigger.EventTrigger; +import org.springframework.statemachine.trigger.TimerTrigger; + +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.time.DurationFormatUtils.formatDurationWords; + + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TransactionHelper { + + private static final Log log = LogFactory.getLog(TransactionHelper.class); + + public static String getTransitionDescription( + @Nullable Transition transition, + PlantUmlWriterParameters plantUmlWriterParameters + ) { + if (transition == null) { + return ""; + } + + return getTransitionDescription( + getTransitionDescriptionEvent(transition), + getTransitionDescriptionGuard(transition), + getTransitionDescriptionActions(transition), + plantUmlWriterParameters.getTransitionLabelSeparator(), + label -> plantUmlWriterParameters.decorateLabel( + transition.getSource() == null ? null : transition.getSource().getId(), + transition.getTarget().getId(), + label + )); + } + + private static String getTransitionDescriptionEvent(Transition transition) { + String event = null; + if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) { + event = transition.getTrigger().getEvent().toString(); + } + + if (transition.getTrigger() instanceof EventTrigger eventTrigger) { + if (eventTrigger.getEvent() != null) { + event = eventTrigger.getEvent().toString(); + } + } else if (transition.getTrigger() instanceof TimerTrigger timerTrigger) { + if (timerTrigger.getCount() == 0) { + event = "every " + + formatDurationWords(timerTrigger.getPeriod(), true, true); + } else { + event = "after " + + formatDurationWords(timerTrigger.getPeriod(), true, true); + + if (timerTrigger.getCount() > 1) { + event += " ( " + timerTrigger.getCount() + "x )"; + } + } + } + return event; + } + + private static String getTransitionDescriptionGuard(Transition transition) { + if (transition.getGuard() != null) { + return "[" + NameGetter.getName(transition.getGuard()) + "]"; + } else { + return null; + } + } + + private static String getTransitionDescriptionActions(Transition transition) { + if (transition.getActions() != null && !transition.getActions().isEmpty()) { + return "/ " + transition.getActions().stream() + .map(NameGetter::getName) + .collect(Collectors.joining(", ")); + } else { + return null; + } + } + + private static String getTransitionDescription( + @Nullable String event, + @Nullable String guard, + @Nullable String actions, + String labelSeparator, + UnaryOperator labelDecorator + ) { + // create guardAndAction based on guard and transaction's actions + String guardAndAction = null; + if (guard != null) { + if (actions != null) { + guardAndAction = guard + labelSeparator + actions; + } else { + guardAndAction = guard; + } + } else { + if (actions != null) { + guardAndAction = actions; + } + } + + // finally, create transition description + if (event != null) { + if (guardAndAction != null) { + return ": " + labelDecorator.apply(event + labelSeparator + guardAndAction); + } else { + return ": " + labelDecorator.apply(event); + } + } else { + if (guardAndAction != null) { + return ": " + labelDecorator.apply(guardAndAction); + } else { + return ""; + } + } + } + + @Nullable + public static Transition getInitialTransition(Region region) { + if (region instanceof AbstractStateMachine) { + try { + return ((Transition) FieldUtils.readField(region, "initialTransition", true)); + } catch (IllegalAccessException ex) { + log.error("error while getting 'initialTransition'", ex); + } + } + return null; + } + +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java new file mode 100644 index 000000000..b8d0060dd --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java @@ -0,0 +1,13 @@ +package org.springframework.statemachine.plantuml.helper; + +import org.springframework.statemachine.transition.Transition; + +import java.util.Comparator; + +public class TransitionComparator implements Comparator> { + + @Override + public int compare(Transition transition1, Transition transition2) { + return transition1.getSource().toString().compareTo(transition2.getTarget().toString()); + } +} diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java new file mode 100644 index 000000000..2c3d0c0f3 --- /dev/null +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java @@ -0,0 +1,320 @@ +package org.springframework.statemachine.plantuml; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.uml.UmlStateMachineModelFactory; +import org.springframework.util.ObjectUtils; + +import java.nio.file.Files; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.statemachine.plantuml.PlantUmlWriterParameters.Direction.UP; + +class PlantUmlWriterTest { + + private static final Log log = LogFactory.getLog(PlantUmlWriterTest.class); + + private static AnnotationConfigApplicationContext context; + + @BeforeAll + public static void setup() { + context = new AnnotationConfigApplicationContext(); + // adding ALL beans needed by ALL .uml files + context.register(BeansForUmlFiles.class); + // refreshing context + context.refresh(); + } + + /** + * Commented UML files correspond to test cases for which PlantUmlWriter is not yet 'ready' + * + * @return stream of "uml resource path" + */ + public static Stream plantUmlTestMethodSource() { + return Stream.of( + Arguments.of("org/springframework/statemachine/uml/action-with-transition-choice", null), + Arguments.of("org/springframework/statemachine/uml/action-with-transition-junction", null), + Arguments.of("org/springframework/statemachine/uml/broken-model-shadowentries", null), + Arguments.of("org/springframework/statemachine/uml/initial-actions", null), + Arguments.of("org/springframework/statemachine/uml/missingname-choice", null), + Arguments.of("org/springframework/statemachine/uml/multijoin-forkjoin", null), + Arguments.of("org/springframework/statemachine/uml/pseudostate-in-submachine", null), + Arguments.of("org/springframework/statemachine/uml/pseudostate-in-submachineref", null), + Arguments.of("org/springframework/statemachine/uml/simple-actions", null), + Arguments.of("org/springframework/statemachine/uml/simple-choice", null), + // It seems Spring statemachine does not support org.eclipse.uml2.uml.ConnectionPointReference ! + // Arguments.of("org/springframework/statemachine/uml/simple-connectionpointref", null), + Arguments.of("org/springframework/statemachine/uml/simple-entryexit", null), + Arguments.of("org/springframework/statemachine/uml/simple-eventdefer", null), + Arguments.of("org/springframework/statemachine/uml/simple-flat-end", null), + Arguments.of("org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices", null), + Arguments.of("org/springframework/statemachine/uml/simple-flat-multiple-to-end", null), + Arguments.of("org/springframework/statemachine/uml/simple-flat", null), + Arguments.of("org/springframework/statemachine/uml/simple-forkjoin", null), + Arguments.of("org/springframework/statemachine/uml/simple-guards", null), + Arguments.of("org/springframework/statemachine/uml/simple-history-deep", null), + Arguments.of("org/springframework/statemachine/uml/simple-history-default", null), + Arguments.of("org/springframework/statemachine/uml/simple-history-shallow", null), + Arguments.of("org/springframework/statemachine/uml/simple-junction", null), + Arguments.of("org/springframework/statemachine/uml/simple-localtransition", + // add some parameters to make diagram more readable + new PlantUmlWriterParameters() + .arrowDirection("S22", UP, "S2") + .arrowDirection("S21", UP, "S2") + .arrowDirection("S22", UP, "S2") + .arrowDirection("S21", UP, "S2") + ), + // It seems Spring statemachine UML parser creates duplicated transitions! + // Arguments.of("org/springframework/statemachine/uml/simple-root-regions", null), + Arguments.of("org/springframework/statemachine/uml/simple-spels", null), + Arguments.of("org/springframework/statemachine/uml/simple-state-actions", null), + Arguments.of("org/springframework/statemachine/uml/simple-submachine", null), + Arguments.of("org/springframework/statemachine/uml/simple-submachineref", null), + Arguments.of("org/springframework/statemachine/uml/simple-timers", null), + Arguments.of("org/springframework/statemachine/uml/simple-transitiontypes", null), + Arguments.of("org/springframework/statemachine/uml/transition-effect-spel", null) + ); + } + + @ParameterizedTest + @MethodSource("plantUmlTestMethodSource") + void plantUmlTest(String resourcePath, PlantUmlWriterParameters plantUmlWriterParameters) throws Exception { + + + // Loading statemachine from uml + Resource umlResource = new ClassPathResource(resourcePath + ".uml"); + assertThat(umlResource.exists()).isTrue(); + UmlStateMachineModelFactory umlStateMachineModelFactory = new UmlStateMachineModelFactory(umlResource); + // make umlStateMachineModelFactory aware of beans available in BeansForUmlFiles.class + umlStateMachineModelFactory.setBeanFactory(context); + + // Dumping statemachine to PlantUml diagram + String stateMachineAsPlantUML = new PlantUmlWriter() + .toPlantUml( + StateMachineHelper.buildStateMachine(umlStateMachineModelFactory), + null, + plantUmlWriterParameters + ); + log.info("\n" + stateMachineAsPlantUML); + + // comparing with expected .puml diagram + + Resource pumlResource = new ClassPathResource(resourcePath + ".puml"); + assertThat(pumlResource.exists()).isTrue(); + + String expectedPlantUmlDiagram = new String(Files.readAllBytes(pumlResource.getFile().toPath())); + assertThat(normalizeNewLines(stateMachineAsPlantUML)) + .isEqualTo(normalizeNewLines(expectedPlantUmlDiagram)); + } + + /** + * Normalizing 'new line characters'
+ * Whatever might be the 'new line character' in the input string (CR/LF/CRLF)
+ * it will be replaced by LF. + * + * @param string String to normalize + * @return input string with line endings being '\n' ( 'LF' ) + */ + private static String normalizeNewLines(String string) { + return string.replace("\r\n", "\n").replace('\r', '\n'); + } + + @Configuration + public static class BeansForUmlFiles { + + @Bean + public ChoiceGuard s2Guard() { + return new ChoiceGuard("s2"); + } + + @Bean + public ChoiceGuard s3Guard() { + return new ChoiceGuard("s3"); + } + + @Bean + public ChoiceGuard s5Guard() { + return new ChoiceGuard("s5"); + } + + @Bean + public ChoiceGuard choice2Guard() { + return new ChoiceGuard("choice2"); + } + + @Bean + public SimpleGuard denyGuard() { + return new SimpleGuard(false); + } + + @Bean + public LatchAction s1ToChoice() { + return new LatchAction(); + } + + @Bean + public LatchAction choiceToS2() { + return new LatchAction(); + } + + @Bean + public LatchAction choiceToS4() { + return new LatchAction(); + } + + @Bean + public LatchAction choice1ToChoice2() { + return new LatchAction(); + } + + @Bean + public LatchAction choiceToS5() { + return new LatchAction(); + } + + @Bean + public LatchAction choiceToS6() { + return new LatchAction(); + } + + @Bean + public LatchAction action1() { + return new LatchAction(); + } + + @Bean + public JunctionGuard s6Guard() { + return new JunctionGuard("s6"); + } + + @Bean + public LatchAction initialAction() { + return new LatchAction(); + } + + @Bean + public LatchAction e1Action() { + return new LatchAction(); + } + + @Bean + public LatchAction e2Action() { + return new LatchAction(); + } + + @Bean + public LatchAction s1Exit() { + return new LatchAction(); + } + + @Bean + public LatchAction s2Entry() { + return new LatchAction(); + } + + @Bean + public LatchAction s3Entry() { + return new LatchAction(); + } + + @Bean + public LatchAction s5Entry() { + return new LatchAction(); + } + } + + // -------------------------------------------------------------------------------- + // Actions + // -------------------------------------------------------------------------------- + + @Setter + @Getter + public static class LatchAction implements Action, BeanNameAware { + + private String beanName; + + CountDownLatch latch = new CountDownLatch(1); + + @Override + public void execute(StateContext context) { + latch.countDown(); + } + + } + + // -------------------------------------------------------------------------------- + // Guards + // -------------------------------------------------------------------------------- + + @Setter + @Getter + public static class ChoiceGuard implements Guard, BeanNameAware { + + private String beanName; + + private final String match; + + public ChoiceGuard(String match) { + this.match = match; + } + + @Override + public boolean evaluate(StateContext context) { + return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("choice", String.class)); + } + } + + @Setter + @Getter + public static class SimpleGuard implements Guard, BeanNameAware { + + private String beanName; + + private final boolean deny; + + public SimpleGuard(boolean deny) { + this.deny = deny; + } + + @Override + public boolean evaluate(StateContext context) { + return deny; + } + } + + @Setter + @Getter + public static class JunctionGuard implements Guard, BeanNameAware { + + private String beanName; + + private final String match; + + public JunctionGuard(String match) { + this.match = match; + } + + @Override + public boolean evaluate(StateContext context) { + return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("junction", String.class)); + } + } + +} \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.png new file mode 100644 index 0000000000000000000000000000000000000000..64493bee630f9968d8e36eb7e4efb4afb5e3b75c GIT binary patch literal 23391 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 +state CHOICE1 <> +note left of CHOICE1 : CHOICE1 +'CHOICE2 <> +state CHOICE2 <> +note left of CHOICE2 : CHOICE2 +state S1 +state S2 +state S3 +state S4 +state S5 +state S6 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> CHOICE1 : E1\n/ s1ToChoice +CHOICE1 -down[#000000]-> CHOICE2 : [choice2Guard]\n/ choice1ToChoice2 +CHOICE2 -down[#000000]-> S6 : / choiceToS6 +CHOICE2 -down[#000000]-> S5 : [s5Guard]\n/ choiceToS5 +CHOICE1 -down[#000000]-> S4 : / choiceToS4 +CHOICE1 -down[#000000]-> S3 : [s3Guard] +CHOICE1 -down[#000000]-> S2 : [s2Guard]\n/ choiceToS2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0cc46b16ac5195728f8a71cc1988576500b796 GIT binary patch literal 24147 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml new file mode 100644 index 000000000..4df00132b --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml @@ -0,0 +1,30 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +'JUNCTION1 <> +state JUNCTION1 <> +note left of JUNCTION1 : JUNCTION1 +'JUNCTION2 <> +state JUNCTION2 <> +note left of JUNCTION2 : JUNCTION2 +state S1 +state S2 +state S3 +state S4 +state S5 +state S6 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> JUNCTION1 : E1\n/ s1ToChoice +JUNCTION1 -down[#000000]-> JUNCTION2 : [choice2Guard]\n/ choice1ToChoice2 +JUNCTION2 -down[#000000]-> S6 : / choiceToS6 +JUNCTION2 -down[#000000]-> S5 : [s5Guard]\n/ choiceToS5 +JUNCTION1 -down[#000000]-> S4 : / choiceToS4 +JUNCTION1 -down[#000000]-> S3 : [s3Guard] +JUNCTION1 -down[#000000]-> S2 : [s2Guard]\n/ choiceToS2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.png new file mode 100644 index 0000000000000000000000000000000000000000..f83100bfa0d9adea6682cab46a04df4940d8d307 GIT binary patch literal 2387 zcmcguc{CJUAI2yO@$y=d!APZLNJ)&cH5f^TXfhK<@*837(CwF7owrz z?cw5uCwO|OKfrnV^?Xv|xcMdU&3b%BBZzw_QEtmV1i;OyHO`d?4 zDn%f}^Q|?;Tway>w9Z9#mX5t!Ec;lq_oT;-s8oex{q17pQ34oQ%$Pkn<2M`Lr#LfF zz=33<;A+-Ml$K~)P(`BU&0;i4>u{2Qf8TGx_?i4+X36xf@&lP)L9Vmzdw(6tCbD%P&r zyYNe%$)V`A1JX28d!sLTNzbsy|4fmnz3>L}LXD~h7rD68ERS60k4RnIClIa1$9>!D zd#?B|^sPG3Ng2L-8`H$8uA?;le6h$>t%l=QD1j2Zu~|OKVug;k#Z^{TUUL8T=aDek z@AR7rd2g?>tG3@d)ZTuJw3850Fa`{{mTng8%?_4-`gHxw=P)$p#?GmD97ml7?n_^{ zQV1Q;6s0gRL?vB)%J`VC9BbK7HUaTgoRbcrr0Mm&K%aOj&Ka)771}Hqnq{U4>r-T< zrL9l<=N#|8E~qph3r0foN5bN@&i=9I#g!0Gm$+8|%2v1*Y#}eYXxySw`nkLnt`P_} z-lDh&&jLna{r%)o<1>Rf98sJ4gLbZopDs}#;kIQ)jHBd7V#PhmDeiE>!6L`-;)}3J zd0Udv|6uET^61bImZi#>pvdkb4E*N4(}Fg-3hn}Td7m?(g@vpvg$KBS#ZlVx z-#XicMVEIGLT3?WSr7ix+`iTe=s6*FntQj=&nWOmC2FZnB>1S#M)ix!V%-vd@er*t%t+*bW6L?PML%yzCKiR$t{d;pc{2vT* zrvdL47`Pn<5cY-5Snp^nVQ@aAv+1KE7W~rP?77i_FH_ni@Z6=O@=iUGW6fg&$SZE0dqs z9;>=}c$88o7%VnVTiet+bZcd@hopD!U5I5>vN2a;U?wzcz#w!_bTKLqZeyZ=`*}45)XOjJKIK9_+5gv5Jd}=aQNr3tiRj zeZgBRCrUYnjBrDSuJ^ba7~I|6O*-#lY-Gg$?N!}@p<}_DOCSx6-sV#vLB?f|@7|kB zKN00lt#TGUp2Ics-zz>Se3ZRyYx}Hl&HyM0g#)enbjs1IJpOfcb~c3im|96 zlb=FA003!eX|3bJPEEwt5H*!D2=B2E2h$%|KfBnmC(vgOWa}~d`V3c-4m4F% zw!}nX#WZ6eIzj7m+}T05;wwNPP=~eLB*dhIyAPKA6bRi|0LG>9uO4b1uH&y5r?amE z*~>!qX7LIMhi2f>j$p=qDM>JiDdVa4<7d}@mgM;?!{;!Q-tqK_TOD@ z?NHqS3Q68l0=R*;>A?SIR{Tc-Eg2x(L2>c>943=_5#1$cFH+RptS?ASOx3ujAF%Q; z2u#Lk$;TsMh%(18pWC-n?L-cZ&dki@7DYVMl!}IyB4HUB8TIw`JOIDMHiF5@$RJS> zQ{As*Gm$W!TWb#Mgf%r?N!TAh|NJFHOL!j6au}__SqSNv$v^@E&ph zT$2?e+z8Ihm9aS|bN2M;3>xo3kpjV4lTk>n%&2@yH}CH?Gg#w${9BVOXGWo#bt vZr*9)4*E@^CQ7PMv7pXej*-NDZYV#0%>GCcVzYwxyWul8xd|;Zc7FOVmo}@Z literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml new file mode 100644 index 000000000..4f2152f02 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml @@ -0,0 +1,14 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..c77e7f84bce55248e8e938267cce59ea05541367 GIT binary patch literal 4106 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml new file mode 100644 index 000000000..31f6506fb --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml @@ -0,0 +1,14 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 + + +[*] -[#000000]-> S1 : / initialAction +S1 -down[#000000]-> S2 : E1 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.png new file mode 100644 index 0000000000000000000000000000000000000000..adf1c7b1902c843f4e1f30d2abe3b74b5c87c326 GIT binary patch literal 9640 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 Rz)z-dv@~?pE7feH{ufPMv=smV literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml new file mode 100644 index 000000000..4cfa8f179 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml @@ -0,0 +1,22 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 +state S3 +state S4 +'choice1 <> +state choice1 <> +note left of choice1 : choice1 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> choice1 : E1 +choice1 -down[#000000]-> S2 : [s2Guard] +choice1 -down[#000000]-> S3 : [s3Guard] +choice1 -down[#000000]-> S4 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8d772c66ccb0a306d848ca800260d9bd6a5351 GIT binary patch literal 26848 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 CSOWe4 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml new file mode 100644 index 000000000..b0e0478a2 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml @@ -0,0 +1,45 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +'S1 <> +state S1 <> +note left of S1 : S1 +state S2 { + state S20 + state S21 + + + [*] -[#000000]-> S20 + state S30 + state S31 + + + [*] -[#000000]-> S30 +} +'S3 <> +state S3 <> +note left of S3 : S3 +state S4 +'SF <> +state SF <> +note left of SF : SF +state SI + + +S1 -down[#000000]-> S20 + +S1 -down[#000000]-> S30 + +[*] -[#000000]-> SI +S20 -down[#000000]-> S21 : E2 +S30 -down[#000000]-> S31 : E3 +SI -down[#000000]-> S1 : E1 +S31 -down[#000000]-> S3 +S21 -down[#000000]-> S3 +S3 -down[#000000]-> S4 : [extendedState.variables.isEmpty()] +S3 -down[#000000]-> SF : [!extendedState.variables.isEmpty()] + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.png new file mode 100644 index 0000000000000000000000000000000000000000..2c5f564a369f08c9ae7e5b9fbe18fa7a50194896 GIT binary patch literal 6446 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml new file mode 100644 index 000000000..22022b860 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml @@ -0,0 +1,23 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 { + 'CHOICE <> + state CHOICE <> + note left of CHOICE : CHOICE + state S11 + state S12 + + + [*] -[#000000]-> S11 +} + + +[*] -[#000000]-> S1 +S11 -down[#000000]-> CHOICE +CHOICE -down[#000000]-> S12 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.png new file mode 100644 index 0000000000000000000000000000000000000000..2c5f564a369f08c9ae7e5b9fbe18fa7a50194896 GIT binary patch literal 6446 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml new file mode 100644 index 000000000..22022b860 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml @@ -0,0 +1,23 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 { + 'CHOICE <> + state CHOICE <> + note left of CHOICE : CHOICE + state S11 + state S12 + + + [*] -[#000000]-> S11 +} + + +[*] -[#000000]-> S1 +S11 -down[#000000]-> CHOICE +CHOICE -down[#000000]-> S12 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..96fd794d981e76284be74b2672da1eb09b29bffd GIT binary patch literal 8171 zcmd^kc{tSn+wYW0DO;ilW8WhovWz|1MRtZP+4tRyy+UCKA%hmOE4vAieV3i=43pgu zMh4H@@B2LGJkL4*JmZp%>MXT%2E8-dol=b6sqK0rLI zD)P`~gN(O4TxjASg_-f;XH&dq8g$@qQ`3UnYp%98m2H{a+DJthJ~N&rV@jwHHm9@y zti)GZ`7we;_l?dQr=nG3`q-v7jj^TvIt%3s8F-@p`AEjqz?qPUd9hD@ANopLV&FGE zbr(|2-Pc zH`=P*KV)UnGdNL07h_l7y27{{5P&qJ)2qu{6?+(besN@vAp>7^Mg0?_=^oa4Gu_5!37%#glSJzN&bmHd}BJmU4QAg-Q%y* z^~>r!QK9nj(HAQPM(OAW^6%WaFu{={QfU&eTSc~_lP5rS;qkeNJL;rKnsOH}afh<& zT-LkAr+(qF!<8cjmMa|b5Ox#32BCm1JQjls_VO|g)ZRSWQ(4CJ@1ni35_hGu&|X)# zX-m$@8S#zNLZ}!PNaRwZC?QYTV36n01PB$^0!bCSN?cE>@SY-UmtheeKn zn68_jvNt@+Auo7pZ6q=?x7E z`zT7DRK}lFP@TIE-m}8qTYAbVBISNyLthd?!#mHBdgYA6Gi+tCI6}81kyiuWpn?xweo>6SpH}i;POG zdf$n0ieU?ra@sbZpUuPhA|EO?U7hG(_NaTX^5mmP75{0$-gi!X^z3_(sOARs`$ z&|t~1mFgP)F*6z#6o_AFU3?W=pYwNr!^v=(QKaMIRXRo~-yYaC0y%!(&9usyUf8N7 zhF)ZUeM0Xy6x2t$l;39F<4L2CJ(X>Qbbh0r%@Wj5N5-_RguxOa2 zez`GO{l&a?6y~YKvCypD7JJ+7OO>gLE92ikD|*$g(=|0U+1c3%>?(9OZq!JjA9yX# zAhvwQrYKSb&EH7}lm%g<9)am%n50JGF4bCUYF)S8>O6=957pRDDyYw&KT|M>b~q%v zI}Cl+d_hM0eR^)6hmVg9MhpiVn`D6E@p#Xm`8sL$Ik~eeN|8OmqpDpdeMuJ-si48~ zNJ6JeEGm*Y&>n!8XYy0=8|vEHDPLMhr=lojF~#`OfZgSn


eTRFM8yU)WLg6pRv z2y&`mOH-o+DqMe7e@Dk7b#$zsoXQfKNBra@8vG4Rv-_m(YD z)T*kg9v&W*PGee{n!UZfm4B(A?(8rkwyI49%kr-i(=3oszaVV?_dgO{r@zI6}*2jgNPHE5Am@;Vv{oY#oXX}ZL1bEs%m4Qhr*G9{nE9AIjIu&Hn5%S;cQylGFr><5#tt zg}*f?0xl+CT*h(#ewA*H+8vV$qgvnM{37$({hl-t#c0&Mdc*uE$_B7L$6Xk6(4T>r zm}|Us+f7$Zt6f2SGsN9y8pB9(xI{$Cjmw|u4rQy-VB&bZi;0bU0u#bMtHXfYlwZ?v za&o41MUYa`UGk=ws`?tUrO&s}EClkWbb(z+=!;3EF+|Ax@ndTI{8=>_g?Rfmza+n7 zS|z#!jwiYHKv1ynN$}s5+#S#UOqsJOD4{T73ypTiOMpx>bphqIovVyB#ngWhD}wO4 zM@Yg&{5O1k!ghOtDto0gy8E$|C{FDyp1dD6L=5SCw2khzrCF%K8qYOdt>wL*?n+b zNaj*6T%W?2T3l?bSeBCnGjjXKi>o)gHc*GxMy7A5A(K|>y#a1nfUEfT+QwqBJpew8 z9Xwk>_S<%~$1$yqms%(D=`#lt=9#2?Z_u4Obrk3qX2)k{W}?w%zfA@~a>Sxg;&tY8NhyOR0$6WH0Pp)e6oYzIKVai@|x39sdzY}Led z>xbEFFyynf(PDmsqOU3D0m4t4o`;8@KT4$rDRD|K6Zj{ZhI>637Xl6XlQ4BOuJZcw zLnJ}_@#BH7Pn&z(0+Md0-**XIE3RHwRJLJgu~kI2Lf`ki(@Ezs)HJC^IpXuno$+N;Rqn{b1<$h$X>o_IuFoaxY>j*^uL; z1YYk5rA3Alcqn&vc4k6OkFU^h)jf757VCXbxLCW&l{()k0j5T0WVk>t5rLR^xX@g3 z-u3@MdC^<^3PlD-kE*8#irN2c9AP?fcj6-L;y++(pzjJMyLvS%aFsWQgpCe&?VV|5 zLGp2h^Tb(6sF5lO^wEn}JL>Zv9Zt@k_-by1MzubOST-nLy$$tCvh;CpcrN7L_a38P z_-*(&>DM{xD6dPs#U>*;o4LajQF3Szd)JWyK_A&1U%3klXnC(f1E2gO-bxZSs(+UH z5~9n{xH=bn#bORE-(2wWzpLjK<50&(2jZ?%<0E-j_Ak7rlqKry9}cKMC&e$BuUlnuCoG+P(Ea6mk5PAd`5)7Mv&UN@r?oam zdskyi%xew4z-2f&bMo>!o?HT>e-C_$yhhLV;%W2C`kgL*t>eR8T*zU!g+YlK22@%W za;LL$!G(P22QSbQeyhX0tgP}SRyL2c)&O~LHk_V}kB|TP3$nqKZY8+0&@ z%S4peEc%d;QV(s4vPc!vg;lO#*rR@Bqd(5OQmmJQkXx2XVd z?NyF6Tj^kmA9|Ob?zGp!)Of(aP|m=~g6(U?7`qo5l`iZdVyplGZ*nI-iWGZ#cr5qO>-qYQ0QAL`qnKp^jfxHEt8dRb+4nd)59cKuc8KT%2M2=? z7Z?=({RBP!ZE^ZHrMAi~Bhhx1B-gxejArp!+WbU3c!|HzC#XZi(R zZ)Ihr5uQqjFB^VTKTSIjO9*w@nyH^X?DNwIe}qh3k7vlQu4Vg14c-3vS@U*u%=oxb zG$pVO`K3~#xf)BFM^!r9|Zzoo)y$PIo|Byw-W5crfdkV_Yo!YV2%00VX#)}C8RC|U!e z0RA000pI`ef&mJyE=By7Y%md63o>f4(PC3O!wKho+F3B4wb9tuuhrGm7#?`q$jgU0bai&FOxOBt{Hg@eJX|mD&y?`^i2clS z3WU@x!np!JWJge3y;xUmQc{xmjQ=75zxam1^WfdPcaru$S%x@l52M*w*@{C5&Ctw% z<+SUjY-BfRX{n)?`CzaUpgjA!0S*oq=yxa4(9nPlxO8!AXBNThzYUQO0B@l~Df8!N zs<5@%ah9e^9HYbd*AM2edE}5RdA;f4>Vhr*SP>=TGb{3G9LTju3MR)fHZm&cPqb_Z z3@D;IcZ$KGfF=sddf&~Y3H}ZW(rJX9feLzg1o&O7Z<~2ydV2bsH)Q~I{Ww38;=|rM zjua%Pq~Pj$Iy+S#J#vRLOM2%a4mJVaN9#q{D}v<#08uhYdQVolTxT>kHnwhy0b#0k zc*CCn@@NzMh^31XY#|NjDSXZz@X z>nHv5fJ_Vq_IaNUVivf+?r`yiA2McXl|)YU13ZWd3K3`_4ZTYZg)}K4A!n@_7LBt9 z(*pzvzfH8cPrGu{tMTrUO z=;(-Vx++xn6|t`1y$n(TSqhTvl& zk=V4*@MsyxOk*_28qcnW>R&!52XEi`3{7}be0*P<%uJC$-}^T4zAP#B`~oF2L}Xxtbb9UXScV6r;s zV}khedjRks1(T$Nn5(C@Xwct3fF&iwJSTUEyZyCe0ps}jidAdK9B&Wd_h3%i2zO>xFdTTZZ{UIeU{#o;+%e_a1FZuu#~4y5(tW<^g{LX@1glUI_^Zmr zX}T|o7uF?u3G(XhqGE{z;0=vK;7oueDbmg9YNnK}vu>x*TfunO`fe~lp>CC#(gC_~ zfKI30jxpd1XtK7JmX4;k7lf0^Z{IJPv*Mc;GpOgS>9%DcfhecqJ+2m;VakBH^xepy zy1$ja~wAM=ACbjumgzG?00oGY?0G%o0@)z!(J zRb){73pB>i15hd7_59q-H(mQ1lTg{$tJR!$TKspzHZPQ zEte*Xh-vZi>dyp5(z8SY5IUA2`zs+9o!~rGQ)=7&7I0#TY4zs%M0v46zxX|yw9cH^ z8?aO2?!#DyfX#xh{%*_E9G{)}BKC;wvL`?QCHijsdN2#Oqv=oh-LIYC0<;iMy7UQQ zG3-=xg0~iB_}vGzfkcXz8?LGz*9Gi4jux%JcrznB_SbJnbH~7*9f(*+YvYU zf!cSVt!C~wZCe@mJz+@HXY~oMM*96;(TkJG0T-fq)4MEXvV!)FA0SmtL5C2f6cYSu zVjj4Nb`*5B9^sceIQ@f<2#kmYsQRHFqI+v&+M_Rzvi&Kz+}$#19zx5H(u6X(hrO~g z*+Q@%v{?)11QE;$rH&(f7%#uE=WIO$ciAn)rv4s+(04);y*+4^YB;LJT?$rOIEdDI-EZZIXwvRbggopuv(qAYXUHD zHW#piWLFd{;z-YCnP>nq%`dUCurb2ZNX8cXqE)sXfDG=*yKvm2Ke*hEDgb$bnsNCD@paBhSHS>25tMvb|d|8hGr$|oI)SqcU}%=e|)6a~Yv&%Ze-P(MS~ zWF1h`(6ICJzbLosmn11RD*brvG1(xt1=GAs&6*YbH@|MG+zu76GNW&4mpfukasN$Y zuM9#G+tWFnZ$_GyqD5+a$J2`yr|tRXkw)<*smKbKp$mAumUYO`lxOATo|c6Lb;3mrjQS zz}S>ZV8Bg|G=zlg4%Qbo_GQjySh8Q4nkvT~d_R!El~ZC;ubI8QSC;}0RNcI3b4eok z@9hUzmG*d6#o(jK*>@>_OQx1AWyXq(^^A~vtmW;$l;~Am8+Y}tO^gz6Iv!SCfcJHv zJ~zsDIf$WP?*4JJUU$O~dR!WFsU%s43jqqrK_*DyoqL+B{a&~LrI%l-Vc41V5krgQz0pELA-ngUH@C+cPV`;-IQ=pan-A<1*|(`#)6&vB z$1Gt-ccC&ZGyZo~EOAXNxXFLgDM`42u<+XRklhdpgp9B2#f$_F4vr4(>zLv7nfluD zWSCZ@2Kz+IAx^m8Jb?Mh*G$kcjHllRCi)nU3aL1|4&1IDV_ET(TNdD)y)lceu`gNq zF?WKWgmWaV-`(~H(rC>r>~`kj0Z1xVgx0NoVFYZKbNmYVR4%tgOr3^a$L>@BC=JkG z2Uvn}QUli@h>9pD=glch-`nUCTo4*)VBkrvmwGn#$#L$!NlaYo(xm&4ltCVA zeGYXw!L2Z$&mOevosE76{x7E-83#bmAm-R{ROL21Lrk>!;KcFzrjRS}cZ{{@Cw7HY z?Y#5+(>4mJDVeo9(s`CLw#=fgQ${7`B&UxXL8$t*J+^d8cx-7VbUv5mi|Sbjb^#uv z^RK&?o)HotEZx5KAhl6(AAL8|dLOy`8h2%HDtDf_U~iCD-DRC!Z~3 z^PR3{+P$TnrEUWq|Mqr3zKL(&KHGvUy%1?u;7)l(&LHX@{EG0J=wDsri7X}_} zR0CmZWB2?z)bIk}KU`KdZCh$AmLO3jT9OJIYi)UT#No%&SCi&P4-d-+!Q|yd$`p^>jNLw2! z?uy=nHbaus4-XGLCxsmbvn>w<;^G$+f%t~|ii0*{v1v6OE$xKc@i!;>Gi3 Q@YWros;s3{qF@>RZ&rT=vj6}9 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml new file mode 100644 index 000000000..1a5580216 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml @@ -0,0 +1,17 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +S1 : /exit s1Exit +state S2 +S2 : /entry s2Entry +S2 : /do extendedState.variables.put('hellos2do','hellos2dovalue') + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1\n/ e1Action + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.png new file mode 100644 index 0000000000000000000000000000000000000000..e513645e4183c3b70007325c38e17133a29ffae0 GIT binary patch literal 9647 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml new file mode 100644 index 000000000..67c984de7 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml @@ -0,0 +1,22 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +'CHOICE <> +state CHOICE <> +note left of CHOICE : CHOICE +state S1 +state S2 +state S3 +state S4 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> CHOICE : E1 +CHOICE -down[#000000]-> S4 +CHOICE -down[#000000]-> S3 : [s3Guard] +CHOICE -down[#000000]-> S2 : [s2Guard] + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.png new file mode 100644 index 0000000000000000000000000000000000000000..ccfc1540dcf41af27e6fbfc119cb459759712707 GIT binary patch literal 25783 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%0D}ja>$cj;rJ*EG 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 OMj2s`>*pPDyzwuKU(#3r literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml new file mode 100644 index 000000000..80adcdbe2 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml @@ -0,0 +1,30 @@ +@startuml +note "!!! NOT WORKING !!!\n!!! Missing 'EXIT -down[#000000]-> S4' transition !!!\n It seems ConnectionPointRef is not supported by Spring Statemachine" as NOT_WORKING +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 { + state S21 + state S22 + + + [*] -[#000000]-> S21 + state ENTRY <> + state EXIT <> +} +state S3 +state S4 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +ENTRY -down[#000000]-> S22 +S22 -down[#000000]-> EXIT : E4 +S1 -down[#000000]-> ENTRY : E3 +S2 -down[#000000]-> S3 : E2 +EXIT -down[#000000]-> S4 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.png new file mode 100644 index 0000000000000000000000000000000000000000..0ebc61b3535cfe8ad9c447ac4e7fc919c7874add GIT binary patch literal 16653 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 KP;lKe@c#n4PJ1{2 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml new file mode 100644 index 000000000..cbdb6cbd4 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 { + state S21 + state S22 + + + [*] -[#000000]-> S21 + state ENTRY <> + state EXIT <> +} +state S3 +state S4 + + +[*] -[#000000]-> S1 +ENTRY -down[#000000]-> S22 +S22 -down[#000000]-> EXIT : E4 +S1 -down[#000000]-> ENTRY : E3 +EXIT -down[#000000]-> S4 +S1 -down[#000000]-> S2 : E1 +S2 -down[#000000]-> S3 : E2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.png new file mode 100644 index 0000000000000000000000000000000000000000..ae8a1cc55802706c6bb9cebcca0fde4f7acbab30 GIT binary patch literal 4494 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 i S1 +S1 -down[#000000]-> S2 : E1 +S2 -down[#000000]-> S3 : E2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.png new file mode 100644 index 0000000000000000000000000000000000000000..7886b7f839f35ef247266bcadcac40093a3e7ce2 GIT binary patch literal 5238 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 HD&+qFK8rRp literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml new file mode 100644 index 000000000..a83a59393 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml @@ -0,0 +1,18 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 +'S3 <> +state S3 <> +note left of S3 : S3 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +S2 -down[#000000]-> S3 : E2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.png new file mode 100644 index 0000000000000000000000000000000000000000..77720385317637ca80949c3d4a3ab7c723ad8e4a GIT binary patch literal 9039 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 x?6CchsQM> +state CHOICE1 <> +note left of CHOICE1 : CHOICE1 +'CHOICE2 <> +state CHOICE2 <> +note left of CHOICE2 : CHOICE2 +'FINAL <> +state FINAL <> +note left of FINAL : FINAL +state S1 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> CHOICE1 +CHOICE2 -down[#000000]-> FINAL +CHOICE1 -down[#000000]-> FINAL +S1 -down[#000000]-> CHOICE2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.png new file mode 100644 index 0000000000000000000000000000000000000000..dc5e7903476464d7ff7a5e3858bdd50d069510c7 GIT binary patch literal 5612 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 bXZp6Z_)T+UG8H&>1wFi{d$&Rj`Qm>76Qiff literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml new file mode 100644 index 000000000..00f8ce967 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml @@ -0,0 +1,20 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +'FINAL1 <> +state FINAL1 <> +note left of FINAL1 : FINAL1 +'FINAL2 <> +state FINAL2 <> +note left of FINAL2 : FINAL2 +state S1 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> FINAL1 +S1 -down[#000000]-> FINAL2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.png new file mode 100644 index 0000000000000000000000000000000000000000..e161116712a8eb8c7708da3853651341aa17b179 GIT binary patch literal 4001 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

-VswccUIfEt1w}RFpV>9z4G#||2GDcE+#dWF_f8%cty?3htkcV?tI<-+ zf4HakJHWl0*A4DbXQw>*8Q=uANUb8qF+2vsm`Y>VsBk3&)VvjyG8kgT`NW&aS!7-2VY^ zE+jC}<`3vNySlpIoYvom-`p}bH1zyx+D2w1r=+~UKY>rWAy7CxJe-o2b`2iaMP>lk zbc#L2-qu!3-WCcqMwzZo1ux(KS{czPmO!9928f8qmv27`2o4 zh&|oiAFh&zhFHC&5{@n%*+k28;8f3_uc@j+F;5mc1F9f@mlGp8D~gOAf!~U}T#^hl z-4SY>*zm3MZ1;#&+JAi3zj5L4BIB!j7Uh&FSVJSl<`*3qxeifbrDJCL#Q&##0h+8& z^PW+jLFZF-wkOMdv#af0T`AWNCb87abMh|L>R2D**2V_=1SdC%c(!5*A%*XoruB>)}5@KURXz#lEUTNo9+&+~*6|_Y7VvnJ? zXWSUHNi^%2@3A_R+7qE3iN{Od*fAY)$35KJfbKIortQDKh`uX3Hl^5v6AQf${FL{uDTbz#E`R!K}wtQPN~ zjd^wSH}tUC0Kb4hUg%+#_03IZySzMDwkH@L5#gDjBHwu27-vq4C{DmM5p_9&K}S9U zjfc&j8bI7|&lUH&!c|V#GWy_d-^4auPkkNyJwk+BX8*S1j8x$C<1Y@ugxp+Kfv^{# z=;^^XJ{U44(n-DcqJl zL4#SU5cGIU(9*#ne!D4n%S(+@@giU%(gePgJC^?porwZRM@N(PncnwFYszcCsaXfp z;k>Ffuu|Q%&l>W;eTGOKxyIKo4hU#I3)STvvzN&Ux5cgx8mB7WebBGB6|zOa0Q>B- zf)s#F{n&-<&%teEJDY&KO@Q3iD#isMQgZT% zgM)i6!Wfo`Rsa+-a~CJVB)gZ&oA=vz_02_wUs5u23kuke_*H2qG`o{WM5ANUd?ba40{j4~6cS-sM`bTj@(|oi1ISw4t0C|Ta``_$RY0*Huz`a?w>jsECxO2* Oi;3aQt7QgGVgCUZq7XX( literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml new file mode 100644 index 000000000..a2de77e7c --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml @@ -0,0 +1,15 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 +S2 : /entry action1 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.png new file mode 100644 index 0000000000000000000000000000000000000000..e521d87cf9943635d724ad18e9f533bd24f35f0c GIT binary patch literal 19517 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml new file mode 100644 index 000000000..e2ab52507 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml @@ -0,0 +1,43 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +'S1 <> +state S1 <> +note left of S1 : S1 +state S2 { + state S20 + state S21 + + + [*] -[#000000]-> S20 + state S30 + state S31 + + + [*] -[#000000]-> S30 +} +'S3 <> +state S3 <> +note left of S3 : S3 +'SF <> +state SF <> +note left of SF : SF +state SI + + +S1 -down[#000000]-> S20 + +S1 -down[#000000]-> S30 + +[*] -[#000000]-> SI +S20 -down[#000000]-> S21 : E2 +S30 -down[#000000]-> S31 : E3 +SI -down[#000000]-> S1 : E1 +S31 -down[#000000]-> S3 +S21 -down[#000000]-> S3 +S3 -down[#000000]-> SF + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.png new file mode 100644 index 0000000000000000000000000000000000000000..f63df1e769a13f11c9bef8030d3e6274cebe496c GIT binary patch literal 9185 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 S1 +S1 -down[#000000]-> S3 : E2 +S1 -down[#000000]-> S2 : E1\n[denyGuard] +S3 -down[#000000]-> S4 : [denyGuard] + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.png new file mode 100644 index 0000000000000000000000000000000000000000..295ef8878c099343909dddd2741fa6f01f5e65c6 GIT binary patch literal 19209 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 S211 + } + + + [*] -[#000000]-> S20 +} + + +[*] -[#000000]-> S1 +S211 -down[#000000]-> S212 : E2 +S212 -down[#000000]-> S1 : E3 +S1 -down[#000000]-> S211 : E1 +'S1 -> SH +S1 -down[#000000]-> S2[H*] : E4 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.png new file mode 100644 index 0000000000000000000000000000000000000000..0615b6807b6060fca0e9974c9f31a797823c1976 GIT binary patch literal 13406 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

- * exemple: - * S1 -left[hidden]-> S2 - */ - private final Map, Direction> hiddenTransitions = new HashMap<>(); - - public PlantUmlWriterParameters hiddenTransition(S source, Direction direction, S target) { - hiddenTransitions.put(new Connection<>(source, target), direction); - return this; - } - - public String getHiddenTransitions() { - String hiddenTransitionsText = hiddenTransitions.entrySet().stream() - .map(hiddenTransition -> "%s -%s[hidden]-> %s" - .formatted( - hiddenTransition.getKey().getSource(), - hiddenTransition.getValue().name().toLowerCase(), - hiddenTransition.getKey().getTarget() - )) - .collect(Collectors.joining("\n")); - - return hiddenTransitionsText.isEmpty() - ? "" - : "\n" + hiddenTransitionsText + "\n"; - } - - @Builder - @Getter - @EqualsAndHashCode - static class LabelDecorator { - String prefix = ""; - String suffix = ""; - - String decorate(String label) { - return prefix + label + suffix; - } - } - - /** - * Map of ( Connection(sourceSate, targetState) -> LabelDecorator(labelPrefix, labelSuffix) ) - */ - private final Map, LabelDecorator> arrowLabelDecorator = new HashMap<>(); - - public PlantUmlWriterParameters arrowLabelDecorator(S source, S target, String prefix, String suffix) { - arrowLabelDecorator.put( - new Connection<>(source, target), - LabelDecorator.builder().prefix(prefix).suffix(suffix).build() - ); - return this; - } - - public String decorateLabel( - @Nullable S source, - @Nullable S target, - @Nullable String transitionLabel - ) { - if (transitionLabel == null) { - return null; - } - - if (source == null && target == null) { - throw new IllegalArgumentException("source and target state cannot both be null!"); - } - Connection sourceAndTarget = new Connection<>(source, target); - if (arrowLabelDecorator.containsKey(sourceAndTarget)) { - return arrowLabelDecorator.get(sourceAndTarget).decorate(transitionLabel); - } - Connection sourceOnly = new Connection<>(source, null); - Connection targetOnly = new Connection<>(null, target); - if (arrowLabelDecorator.containsKey(sourceOnly) - && arrowLabelDecorator.containsKey(targetOnly) - && !arrowLabelDecorator.get(sourceOnly).equals(arrowLabelDecorator.get(targetOnly)) - ) { - log.warn( - String.format("Two 'unary' 'arrowLabelDecorator' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); - return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); - } else if (arrowLabelDecorator.containsKey(sourceOnly)) { - return arrowLabelDecorator.get(sourceOnly).decorate(transitionLabel); - } else if (arrowLabelDecorator.containsKey(targetOnly)) { - return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); - } else { - return transitionLabel; - } - } - - // Colors - - private String defaultStateColor = ""; - private String currentStateColor = "#FFFF77"; - - @Getter - private String transitionLabelSeparator = "\\n"; - - public PlantUmlWriterParameters setTransitionLabelSeparator(String transitionLabelSeparator) { - this.transitionLabelSeparator = transitionLabelSeparator; - return this; - } - - public PlantUmlWriterParameters defaultStateColor(String defaultStateColor) { - this.defaultStateColor = defaultStateColor; - return this; - } - - public PlantUmlWriterParameters currentStateColor(String currentStateColor) { - this.currentStateColor = currentStateColor; - return this; - } - - public String getStateColor(S state, @Nullable S currentState) { - if (state == null) { - log.warn("null state!"); - return ""; // TODO check why this is happening - } - return state.equals(currentState) ? currentStateColor : defaultStateColor; - } - - public String getArrowColor(boolean isCurrentTransaction) { - return isCurrentTransaction ? "[#FF0000]" : ""; - } + private static final Log log = LogFactory.getLog(PlantUmlWriterParameters.class); + + public static final String DEFAULT_STATE_DIAGRAM_SETTINGS = """ + 'https://plantuml.com/state-diagram + + 'hide description area for state without description + hide empty description + + 'https://plantuml.com/fr/skinparam + 'https://plantuml-documentation.readthedocs.io/en/latest/formatting/all-skin-params.html + 'https://plantuml.com/fr/color + skinparam BackgroundColor white + skinparam DefaultFontColor black + 'skinparam DefaultFontName Impact + skinparam DefaultFontSize 14 + skinparam DefaultFontStyle Normal + skinparam NoteBackgroundColor #FEFFDD + skinparam NoteBorderColor black + + skinparam state { + ArrowColor black + BackgroundColor #F1F1F1 + BorderColor #181818 + FontColor black + ' FontName Impact + FontSize 14 + FontStyle Normal + } + """; + + @Setter + private String stateDiagramSettings = DEFAULT_STATE_DIAGRAM_SETTINGS; + + public static String getStateDiagramSettings(@Nullable PlantUmlWriterParameters plantUmlWriterParameters) { + return plantUmlWriterParameters == null + ? DEFAULT_STATE_DIAGRAM_SETTINGS + : plantUmlWriterParameters.getStateDiagramSettings(); + } + + private String getStateDiagramSettings() { + return stateDiagramSettings == null + ? "" + : stateDiagramSettings; + } + + /** + * Direction of an arrow connecting 2 States + */ + public enum Direction { + UP, + DOWN, + LEFT, + RIGHT + } + + + @Value + @EqualsAndHashCode + private static class Connection { + S source; + S target; + } + + /** + * Map of ( (sourceSate, targetState) -> Direction ) + */ + private final Map, Direction> arrows = new HashMap<>(); + + public PlantUmlWriterParameters arrowDirection(S source, Direction direction, S target) { + arrows.put(new Connection<>(source, target), direction); + return this; + } + + /** + * At least one of source and target must be non-null
+ * See implementation for 'arrow direction rule priority' + * + * @param source source State + * @param target target State + * @return Direction.name() + */ + String getDirection(S source, S target) { + if (source == null && target == null) { + throw new IllegalArgumentException("source and target state cannot both be null!"); + } + Connection sourceAndTarget = new Connection<>(source, target); + if (arrows.containsKey(sourceAndTarget)) { + return arrows.get(sourceAndTarget).name().toLowerCase(); + } + Connection sourceOnly = new Connection<>(source, null); + Connection targetOnly = new Connection<>(null, target); + if (arrows.containsKey(sourceOnly) + && arrows.containsKey(targetOnly) + && !arrows.get(sourceOnly).equals(arrows.get(targetOnly)) + ) { + log.warn( + String.format("Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); + return arrows.get(targetOnly).name().toLowerCase(); + } else if (arrows.containsKey(sourceOnly)) { + return arrows.get(sourceOnly).name().toLowerCase(); + } else if (arrows.containsKey(targetOnly)) { + return arrows.get(targetOnly).name().toLowerCase(); + } else { + return Direction.DOWN.name().toLowerCase(); + } + } + + /** + * Map of ( Connection(sourceSate, targetState) -> Direction ) + * Used to add EXTRA HIDDEN arrows, which are just helping with diagram layout.
+ * This is typically useful to 'force' the position of a state comparing to another, EVEN IF THESE TWO STATES AR NOT CONNECTED in the statemachine :-)
+ *

+ * exemple: + * S1 -left[hidden]-> S2 + */ + private final Map, Direction> hiddenTransitions = new HashMap<>(); + + public PlantUmlWriterParameters hiddenTransition(S source, Direction direction, S target) { + hiddenTransitions.put(new Connection<>(source, target), direction); + return this; + } + + public String getHiddenTransitions() { + String hiddenTransitionsText = hiddenTransitions.entrySet().stream() + .map(hiddenTransition -> "%s -%s[hidden]-> %s" + .formatted( + hiddenTransition.getKey().getSource(), + hiddenTransition.getValue().name().toLowerCase(), + hiddenTransition.getKey().getTarget() + )) + .collect(Collectors.joining("\n")); + + return hiddenTransitionsText.isEmpty() + ? "" + : "\n" + hiddenTransitionsText + "\n"; + } + + @Builder + @Getter + @EqualsAndHashCode + static class LabelDecorator { + String prefix = ""; + String suffix = ""; + + String decorate(String label) { + return prefix + label + suffix; + } + } + + /** + * Map of ( Connection(sourceSate, targetState) -> LabelDecorator(labelPrefix, labelSuffix) ) + */ + private final Map, LabelDecorator> arrowLabelDecorator = new HashMap<>(); + + public PlantUmlWriterParameters arrowLabelDecorator(S source, S target, String prefix, String suffix) { + arrowLabelDecorator.put( + new Connection<>(source, target), + LabelDecorator.builder().prefix(prefix).suffix(suffix).build() + ); + return this; + } + + public String decorateLabel( + @Nullable S source, + @Nullable S target, + @Nullable String transitionLabel + ) { + if (transitionLabel == null) { + return null; + } + + if (source == null && target == null) { + throw new IllegalArgumentException("source and target state cannot both be null!"); + } + Connection sourceAndTarget = new Connection<>(source, target); + if (arrowLabelDecorator.containsKey(sourceAndTarget)) { + return arrowLabelDecorator.get(sourceAndTarget).decorate(transitionLabel); + } + Connection sourceOnly = new Connection<>(source, null); + Connection targetOnly = new Connection<>(null, target); + if (arrowLabelDecorator.containsKey(sourceOnly) + && arrowLabelDecorator.containsKey(targetOnly) + && !arrowLabelDecorator.get(sourceOnly).equals(arrowLabelDecorator.get(targetOnly)) + ) { + log.warn( + String.format("Two 'unary' 'arrowLabelDecorator' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); + return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); + } else if (arrowLabelDecorator.containsKey(sourceOnly)) { + return arrowLabelDecorator.get(sourceOnly).decorate(transitionLabel); + } else if (arrowLabelDecorator.containsKey(targetOnly)) { + return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); + } else { + return transitionLabel; + } + } + + // Colors + + private String defaultStateColor = ""; + private String currentStateColor = "#FFFF77"; + + @Getter + private String transitionLabelSeparator = "\\n"; + + public PlantUmlWriterParameters setTransitionLabelSeparator(String transitionLabelSeparator) { + this.transitionLabelSeparator = transitionLabelSeparator; + return this; + } + + public PlantUmlWriterParameters defaultStateColor(String defaultStateColor) { + this.defaultStateColor = defaultStateColor; + return this; + } + + public PlantUmlWriterParameters currentStateColor(String currentStateColor) { + this.currentStateColor = currentStateColor; + return this; + } + + public String getStateColor(S state, @Nullable S currentState) { + if (state == null) { + log.warn("null state!"); + return ""; // TODO check why this is happening + } + return state.equals(currentState) ? currentStateColor : defaultStateColor; + } + + public String getArrowColor(boolean isCurrentTransaction) { + return isCurrentTransaction ? "[#FF0000]" : ""; + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java index 4410b4c83..f914f1c4f 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java @@ -1,8 +1,23 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.config.StateMachineBuilder; @@ -21,96 +36,96 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class StateMachineHelper { - public static StateMachine buildStateMachine( - StateMachineModelFactory stateMachineModelFactory - ) throws Exception { - StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); - builder.configureModel().withModel().factory(stateMachineModelFactory); - builder.configureConfiguration().withConfiguration(); - return builder.build(); - } + public static StateMachine buildStateMachine( + StateMachineModelFactory stateMachineModelFactory + ) throws Exception { + StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); + builder.configureModel().withModel().factory(stateMachineModelFactory); + builder.configureConfiguration().withConfiguration(); + return builder.build(); + } - public static List getCurrentStates(StateMachine stateMachine) { - ArrayList currentState = new ArrayList<>(); - collectCurrentStates(stateMachine, currentState); - return currentState; - } + public static List getCurrentStates(StateMachine stateMachine) { + ArrayList currentState = new ArrayList<>(); + collectCurrentStates(stateMachine, currentState); + return currentState; + } - private static void collectCurrentStates( - Region region, - ArrayList currentStateAccumulator - ) { - if (region.getState() != null) { - currentStateAccumulator.add(region.getState().getId()); - } + private static void collectCurrentStates( + Region region, + ArrayList currentStateAccumulator + ) { + if (region.getState() != null) { + currentStateAccumulator.add(region.getState().getId()); + } - region.getStates().forEach(state -> { - if (state.isSubmachineState()) { - if (state instanceof AbstractState abstractState) { - collectCurrentStates(abstractState.getSubmachine(), currentStateAccumulator); - } - } else if (state.isOrthogonal() || state.isComposite()) { - if (state instanceof RegionState regionState) { - regionState.getRegions().stream() - .toList() - .forEach(subRegion -> collectCurrentStates(subRegion, currentStateAccumulator)); - } - } - }); - } + region.getStates().forEach(state -> { + if (state.isSubmachineState()) { + if (state instanceof AbstractState abstractState) { + collectCurrentStates(abstractState.getSubmachine(), currentStateAccumulator); + } + } else if (state.isOrthogonal() || state.isComposite()) { + if (state instanceof RegionState regionState) { + regionState.getRegions().stream() + .toList() + .forEach(subRegion -> collectCurrentStates(subRegion, currentStateAccumulator)); + } + } + }); + } - public static Map, String> collectHistoryStates(StateMachine stateMachine) { - HashMap, String> historyStatesToHistoryId = new HashMap, String>(); - collectHistoryStates(stateMachine, null, historyStatesToHistoryId); - return historyStatesToHistoryId; - } + public static Map, String> collectHistoryStates(StateMachine stateMachine) { + HashMap, String> historyStatesToHistoryId = new HashMap, String>(); + collectHistoryStates(stateMachine, null, historyStatesToHistoryId); + return historyStatesToHistoryId; + } - private static void collectHistoryStates( - Region region, - @Nullable State parentState, - Map, String> historyStatesToHistoryId - ) { - region.getStates().forEach(state -> { - if (state.isSimple()) { - collectHistoryState(state, parentState, historyStatesToHistoryId); - } else if (state.isSubmachineState()) { - if (state instanceof AbstractState abstractState) { - collectHistoryStates(abstractState.getSubmachine(), state, historyStatesToHistoryId); - } - } else if (state.isOrthogonal() || state.isComposite()) { - if (state instanceof RegionState regionState) { - regionState.getRegions().stream() - .toList() - .forEach(subRegion -> collectHistoryStates(subRegion, state, historyStatesToHistoryId)); - } - } - }); - } + private static void collectHistoryStates( + Region region, + @Nullable State parentState, + Map, String> historyStatesToHistoryId + ) { + region.getStates().forEach(state -> { + if (state.isSimple()) { + collectHistoryState(state, parentState, historyStatesToHistoryId); + } else if (state.isSubmachineState()) { + if (state instanceof AbstractState abstractState) { + collectHistoryStates(abstractState.getSubmachine(), state, historyStatesToHistoryId); + } + } else if (state.isOrthogonal() || state.isComposite()) { + if (state instanceof RegionState regionState) { + regionState.getRegions().stream() + .toList() + .forEach(subRegion -> collectHistoryStates(subRegion, state, historyStatesToHistoryId)); + } + } + }); + } - private static void collectHistoryState( - State state, - @Nullable State parentState, - Map, String> historyStatesToHistoryId - ) { - if (state.getPseudoState() != null - && ( - state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP - || state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW - ) - ) { - historyStatesToHistoryId.put(state, historyId(parentState, state.getPseudoState().getKind())); - } - } + private static void collectHistoryState( + State state, + @Nullable State parentState, + Map, String> historyStatesToHistoryId + ) { + if (state.getPseudoState() != null + && ( + state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP + || state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW + ) + ) { + historyStatesToHistoryId.put(state, historyId(parentState, state.getPseudoState().getKind())); + } + } - private static String historyId( - @Nullable State parentState, - PseudoStateKind pseudoStateKind - ) { - String prefix = parentState == null ? "" : parentState.getId().toString(); - return switch (pseudoStateKind) { - case HISTORY_DEEP -> prefix + "[H*]"; - case HISTORY_SHALLOW -> prefix + "[H]"; - default -> throw new IllegalArgumentException("pseudoStateKind must be an 'history'"); - }; - } + private static String historyId( + @Nullable State parentState, + PseudoStateKind pseudoStateKind + ) { + String prefix = parentState == null ? "" : parentState.getId().toString(); + return switch (pseudoStateKind) { + case HISTORY_DEEP -> prefix + "[H*]"; + case HISTORY_SHALLOW -> prefix + "[H]"; + default -> throw new IllegalArgumentException("pseudoStateKind must be an 'history'"); + }; + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java index bc31a3650..824dbcea3 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java @@ -1,8 +1,23 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml.helper; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.reflect.FieldUtils; @@ -21,122 +36,122 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class NameGetter { - private static final Log log = LogFactory.getLog(NameGetter.class); - - /** - * Implement a 'name strategy' based on this sequence: - *

    - *
  • {@link Expression}
  • - *
  • {@link BeanNameAware}
  • - *
  • Lambda's unique "arg$1" parameter
  • - *
- * If all these strategy are failing, fallback to "class name" of the object - * - * @param object object to get "name" from - * @return name of the object - */ - public static String getName(Object object) { - String name = getSpellExpression(object); - - if (name == null) { - Object uniqueArg = extractArg$1(object, Action.class); - if (uniqueArg != object) { - return getName(uniqueArg); - } - } - - if (name == null) { - Object uniqueArg = extractArg$1(object, Guard.class); - if (uniqueArg != object) { - return getName(uniqueArg); - } - } - if (name == null) { - name = getBeanName(object); - } - - // fallback - if (name == null) { - name = object.getClass().toString(); - name = name.replace("class ", ""); - // remove trailing "/0x..." in class name - // example: "Actions$$Lambda$504/0x0000000800ec3e00" - name = RegExUtils.removeAll(name, "/0x.*"); - // remove trailing "$(0-9)" in action name - // example: "Actions$$Lambda$504" + private static final Log log = LogFactory.getLog(NameGetter.class); + + /** + * Implement a 'name strategy' based on this sequence: + *
    + *
  • {@link Expression}
  • + *
  • {@link BeanNameAware}
  • + *
  • Lambda's unique "arg$1" parameter
  • + *
+ * If all these strategy are failing, fallback to "class name" of the object + * + * @param object object to get "name" from + * @return name of the object + */ + public static String getName(Object object) { + String name = getSpellExpression(object); + + if (name == null) { + Object uniqueArg = extractArg$1(object, Action.class); + if (uniqueArg != object) { + return getName(uniqueArg); + } + } + + if (name == null) { + Object uniqueArg = extractArg$1(object, Guard.class); + if (uniqueArg != object) { + return getName(uniqueArg); + } + } + if (name == null) { + name = getBeanName(object); + } + + // fallback + if (name == null) { + name = object.getClass().toString(); + name = name.replace("class ", ""); + // remove trailing "/0x..." in class name + // example: "Actions$$Lambda$504/0x0000000800ec3e00" + name = RegExUtils.removeAll(name, "/0x.*"); + // remove trailing "$(0-9)" in action name + // example: "Actions$$Lambda$504" // name = RegExUtils.removeAll(name, "\\$\\d+"); - // only keep class name - // a.b.c.D$32/0x25764366 -> D$32 - String nameWithoutDollar = name; - int indexOfDollarSymbol = name.indexOf("$"); - if (indexOfDollarSymbol != -1) { - nameWithoutDollar = name.substring(0, indexOfDollarSymbol); - } - int lastIndexOfDot = nameWithoutDollar.lastIndexOf("."); - if (lastIndexOfDot != -1) { - name = name.substring(lastIndexOfDot + 1, name.length()); - } - } - return name; - } - - @Nullable - private static String getSpellExpression(Object object) { - if (object instanceof SpelExpressionAction || object instanceof SpelExpressionGuard) { - try { - return ((Expression) FieldUtils.readDeclaredField(object, "expression", true)).getExpressionString(); - } catch (IllegalAccessException ex) { - log.error("error while getting SpelExpression", ex); - } - } - return null; - } - - - // Disable "Rename this ... variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'" - // we want to the name of this method to clearly state what is does - @SuppressWarnings({"squid:S100", "squid:S117"}) - private static Object extractArg$1(Object actionWrappedInFunction, Class typeOfArg) { - Field arg$1Field = FieldUtils.getDeclaredField(actionWrappedInFunction.getClass(), "arg$1", true); - if (arg$1Field != null && arg$1Field.getType() == typeOfArg) { - try { - return FieldUtils.readDeclaredField(actionWrappedInFunction, "arg$1", true); - } catch (IllegalAccessException ex) { - log.error("Error while extracting action from function!", ex); - } - } - return actionWrappedInFunction; - } - - @Nullable - private static String getBeanName(Object object) { - Class clazz = object.getClass(); - if (!ClassUtils.getAllInterfaces(clazz).contains(BeanNameAware.class)) { - log.error("Class " + clazz + " doesn't implements " + BeanNameAware.class + "! " + - "Make sure " + clazz + " implements BeanNameAware AND contains a 'String beanName' field." - ); - return null; - } - - try { - // 'clazz' implements BeanNameAware .. let's try to get beanName - Field beanNameFielOfClazz = FieldUtils.getField(clazz, "beanName", true); - if (beanNameFielOfClazz == null) { - log.error("Class " + clazz + " does NOT contains a 'beanNameAware' field! " + - "Make sure " + clazz + " contains a 'String beanName' field." - ); - } else if (beanNameFielOfClazz.getType() == String.class) { - return (String) FieldUtils.readField(object, "beanName", true); - } else { - log.error("Class " + clazz + " contains a '" + beanNameFielOfClazz.getType() + " beanNameAware' field, but type should be 'String'! " + - "Make sure " + clazz + " contains a 'String beanName' field." - ); - } - } catch (IllegalAccessException ex) { - log.error("Error while accessing field!", ex); - } - - return null; - } + // only keep class name + // a.b.c.D$32/0x25764366 -> D$32 + String nameWithoutDollar = name; + int indexOfDollarSymbol = name.indexOf("$"); + if (indexOfDollarSymbol != -1) { + nameWithoutDollar = name.substring(0, indexOfDollarSymbol); + } + int lastIndexOfDot = nameWithoutDollar.lastIndexOf("."); + if (lastIndexOfDot != -1) { + name = name.substring(lastIndexOfDot + 1, name.length()); + } + } + return name; + } + + @Nullable + private static String getSpellExpression(Object object) { + if (object instanceof SpelExpressionAction || object instanceof SpelExpressionGuard) { + try { + return ((Expression) FieldUtils.readDeclaredField(object, "expression", true)).getExpressionString(); + } catch (IllegalAccessException ex) { + log.error("error while getting SpelExpression", ex); + } + } + return null; + } + + + // Disable "Rename this ... variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'" + // we want to the name of this method to clearly state what is does + @SuppressWarnings({"squid:S100", "squid:S117"}) + private static Object extractArg$1(Object actionWrappedInFunction, Class typeOfArg) { + Field arg$1Field = FieldUtils.getDeclaredField(actionWrappedInFunction.getClass(), "arg$1", true); + if (arg$1Field != null && arg$1Field.getType() == typeOfArg) { + try { + return FieldUtils.readDeclaredField(actionWrappedInFunction, "arg$1", true); + } catch (IllegalAccessException ex) { + log.error("Error while extracting action from function!", ex); + } + } + return actionWrappedInFunction; + } + + @Nullable + private static String getBeanName(Object object) { + Class clazz = object.getClass(); + if (!ClassUtils.getAllInterfaces(clazz).contains(BeanNameAware.class)) { + log.error("Class " + clazz + " doesn't implements " + BeanNameAware.class + "! " + + "Make sure " + clazz + " implements BeanNameAware AND contains a 'String beanName' field." + ); + return null; + } + + try { + // 'clazz' implements BeanNameAware .. let's try to get beanName + Field beanNameFielOfClazz = FieldUtils.getField(clazz, "beanName", true); + if (beanNameFielOfClazz == null) { + log.error("Class " + clazz + " does NOT contains a 'beanNameAware' field! " + + "Make sure " + clazz + " contains a 'String beanName' field." + ); + } else if (beanNameFielOfClazz.getType() == String.class) { + return (String) FieldUtils.readField(object, "beanName", true); + } else { + log.error("Class " + clazz + " contains a '" + beanNameFielOfClazz.getType() + " beanNameAware' field, but type should be 'String'! " + + "Make sure " + clazz + " contains a 'String beanName' field." + ); + } + } catch (IllegalAccessException ex) { + log.error("Error while accessing field!", ex); + } + + return null; + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java index 1bdb42f34..c0082c7c2 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/RegionComparator.java @@ -1,7 +1,22 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml.helper; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.statemachine.region.Region; @@ -14,30 +29,30 @@ @RequiredArgsConstructor public class RegionComparator implements Comparator> { - private static final Log log = LogFactory.getLog(RegionComparator.class); + private static final Log log = LogFactory.getLog(RegionComparator.class); - private final StateComparator stateComparator; + private final StateComparator stateComparator; - @Override - public int compare(Region region1, Region region2) { - List> sourceSortedStates = region1.getStates().stream().sorted(stateComparator).toList(); - List> targetSortedStates = region2.getStates().stream().sorted(stateComparator).toList(); + @Override + public int compare(Region region1, Region region2) { + List> sourceSortedStates = region1.getStates().stream().sorted(stateComparator).toList(); + List> targetSortedStates = region2.getStates().stream().sorted(stateComparator).toList(); - List regionComparisonResult = IntStream - .range(0, Math.min(sourceSortedStates.size(), targetSortedStates.size())) - // comparing pairs of states - .mapToObj(i -> sourceSortedStates.get(i).getId().toString().compareTo(targetSortedStates.get(i).getId().toString())) - .toList(); + List regionComparisonResult = IntStream + .range(0, Math.min(sourceSortedStates.size(), targetSortedStates.size())) + // comparing pairs of states + .mapToObj(i -> sourceSortedStates.get(i).getId().toString().compareTo(targetSortedStates.get(i).getId().toString())) + .toList(); - // returning first "non 0" comparison result - for (Integer comparisonResult : regionComparisonResult) { - if (comparisonResult != 0) { - return comparisonResult; - } - } + // returning first "non 0" comparison result + for (Integer comparisonResult : regionComparisonResult) { + if (comparisonResult != 0) { + return comparisonResult; + } + } - // this should not happen...? - log.warn("getRegionComparator: unable to compare regions!!"); - return 0; - } + // this should not happen...? + log.warn("getRegionComparator: unable to compare regions!!"); + return 0; + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java index 964ac28f7..6e717147c 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/StateComparator.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml.helper; import org.springframework.statemachine.state.State; @@ -5,8 +21,8 @@ import java.util.Comparator; public class StateComparator implements Comparator> { - @Override - public int compare(State state1, State state2) { - return state1.getId().toString().compareTo(state2.getId().toString()); - } + @Override + public int compare(State state1, State state2) { + return state1.getId().toString().compareTo(state2.getId().toString()); + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java index 854bb6627..4890d4f80 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransactionHelper.java @@ -1,15 +1,29 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml.helper; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; import org.springframework.statemachine.plantuml.PlantUmlWriterParameters; import org.springframework.statemachine.region.Region; -import org.springframework.statemachine.state.AbstractState; import org.springframework.statemachine.support.AbstractStateMachine; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.trigger.EventTrigger; @@ -24,119 +38,119 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TransactionHelper { - private static final Log log = LogFactory.getLog(TransactionHelper.class); - - public static String getTransitionDescription( - @Nullable Transition transition, - PlantUmlWriterParameters plantUmlWriterParameters - ) { - if (transition == null) { - return ""; - } - - return getTransitionDescription( - getTransitionDescriptionEvent(transition), - getTransitionDescriptionGuard(transition), - getTransitionDescriptionActions(transition), - plantUmlWriterParameters.getTransitionLabelSeparator(), - label -> plantUmlWriterParameters.decorateLabel( - transition.getSource() == null ? null : transition.getSource().getId(), - transition.getTarget().getId(), - label - )); - } - - private static String getTransitionDescriptionEvent(Transition transition) { - String event = null; - if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) { - event = transition.getTrigger().getEvent().toString(); - } - - if (transition.getTrigger() instanceof EventTrigger eventTrigger) { - if (eventTrigger.getEvent() != null) { - event = eventTrigger.getEvent().toString(); - } - } else if (transition.getTrigger() instanceof TimerTrigger timerTrigger) { - if (timerTrigger.getCount() == 0) { - event = "every " + - formatDurationWords(timerTrigger.getPeriod(), true, true); - } else { - event = "after " - + formatDurationWords(timerTrigger.getPeriod(), true, true); - - if (timerTrigger.getCount() > 1) { - event += " ( " + timerTrigger.getCount() + "x )"; - } - } - } - return event; - } - - private static String getTransitionDescriptionGuard(Transition transition) { - if (transition.getGuard() != null) { - return "[" + NameGetter.getName(transition.getGuard()) + "]"; - } else { - return null; - } - } - - private static String getTransitionDescriptionActions(Transition transition) { - if (transition.getActions() != null && !transition.getActions().isEmpty()) { - return "/ " + transition.getActions().stream() - .map(NameGetter::getName) - .collect(Collectors.joining(", ")); - } else { - return null; - } - } - - private static String getTransitionDescription( - @Nullable String event, - @Nullable String guard, - @Nullable String actions, - String labelSeparator, - UnaryOperator labelDecorator - ) { - // create guardAndAction based on guard and transaction's actions - String guardAndAction = null; - if (guard != null) { - if (actions != null) { - guardAndAction = guard + labelSeparator + actions; - } else { - guardAndAction = guard; - } - } else { - if (actions != null) { - guardAndAction = actions; - } - } - - // finally, create transition description - if (event != null) { - if (guardAndAction != null) { - return ": " + labelDecorator.apply(event + labelSeparator + guardAndAction); - } else { - return ": " + labelDecorator.apply(event); - } - } else { - if (guardAndAction != null) { - return ": " + labelDecorator.apply(guardAndAction); - } else { - return ""; - } - } - } - - @Nullable - public static Transition getInitialTransition(Region region) { - if (region instanceof AbstractStateMachine) { - try { - return ((Transition) FieldUtils.readField(region, "initialTransition", true)); - } catch (IllegalAccessException ex) { - log.error("error while getting 'initialTransition'", ex); - } - } - return null; - } + private static final Log log = LogFactory.getLog(TransactionHelper.class); + + public static String getTransitionDescription( + @Nullable Transition transition, + PlantUmlWriterParameters plantUmlWriterParameters + ) { + if (transition == null) { + return ""; + } + + return getTransitionDescription( + getTransitionDescriptionEvent(transition), + getTransitionDescriptionGuard(transition), + getTransitionDescriptionActions(transition), + plantUmlWriterParameters.getTransitionLabelSeparator(), + label -> plantUmlWriterParameters.decorateLabel( + transition.getSource() == null ? null : transition.getSource().getId(), + transition.getTarget().getId(), + label + )); + } + + private static String getTransitionDescriptionEvent(Transition transition) { + String event = null; + if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) { + event = transition.getTrigger().getEvent().toString(); + } + + if (transition.getTrigger() instanceof EventTrigger eventTrigger) { + if (eventTrigger.getEvent() != null) { + event = eventTrigger.getEvent().toString(); + } + } else if (transition.getTrigger() instanceof TimerTrigger timerTrigger) { + if (timerTrigger.getCount() == 0) { + event = "every " + + formatDurationWords(timerTrigger.getPeriod(), true, true); + } else { + event = "after " + + formatDurationWords(timerTrigger.getPeriod(), true, true); + + if (timerTrigger.getCount() > 1) { + event += " ( " + timerTrigger.getCount() + "x )"; + } + } + } + return event; + } + + private static String getTransitionDescriptionGuard(Transition transition) { + if (transition.getGuard() != null) { + return "[" + NameGetter.getName(transition.getGuard()) + "]"; + } else { + return null; + } + } + + private static String getTransitionDescriptionActions(Transition transition) { + if (transition.getActions() != null && !transition.getActions().isEmpty()) { + return "/ " + transition.getActions().stream() + .map(NameGetter::getName) + .collect(Collectors.joining(", ")); + } else { + return null; + } + } + + private static String getTransitionDescription( + @Nullable String event, + @Nullable String guard, + @Nullable String actions, + String labelSeparator, + UnaryOperator labelDecorator + ) { + // create guardAndAction based on guard and transaction's actions + String guardAndAction = null; + if (guard != null) { + if (actions != null) { + guardAndAction = guard + labelSeparator + actions; + } else { + guardAndAction = guard; + } + } else { + if (actions != null) { + guardAndAction = actions; + } + } + + // finally, create transition description + if (event != null) { + if (guardAndAction != null) { + return ": " + labelDecorator.apply(event + labelSeparator + guardAndAction); + } else { + return ": " + labelDecorator.apply(event); + } + } else { + if (guardAndAction != null) { + return ": " + labelDecorator.apply(guardAndAction); + } else { + return ""; + } + } + } + + @Nullable + public static Transition getInitialTransition(Region region) { + if (region instanceof AbstractStateMachine) { + try { + return ((Transition) FieldUtils.readField(region, "initialTransition", true)); + } catch (IllegalAccessException ex) { + log.error("error while getting 'initialTransition'", ex); + } + } + return null; + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java index 484711b4e..99c9b4338 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml.helper; import org.springframework.statemachine.transition.Transition; @@ -6,14 +22,14 @@ public class TransitionComparator implements Comparator> { - @Override - public int compare(Transition transition1, Transition transition2) { - // First compare by source - int compareBySource = transition1.getSource().toString().compareTo(transition2.getSource().toString()); - if (compareBySource != 0) { - return compareBySource; - } - // If sources are equal, then compare by target - return transition1.getTarget().toString().compareTo(transition2.getTarget().toString()); - } + @Override + public int compare(Transition transition1, Transition transition2) { + // First compare by source + int compareBySource = transition1.getSource().toString().compareTo(transition2.getSource().toString()); + if (compareBySource != 0) { + return compareBySource; + } + // If sources are equal, then compare by target + return transition1.getTarget().toString().compareTo(transition2.getTarget().toString()); + } } From bbab08ec1eca4477ba8a7d91c9cc619ffc7c535f Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Wed, 17 Jul 2024 16:00:11 +0200 Subject: [PATCH 13/16] chore: make attributes private --- .../statemachine/plantuml/ContextTransition.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java index c11a06cd7..94b19faa9 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java @@ -24,9 +24,9 @@ @AllArgsConstructor @Getter public class ContextTransition { - final S source; - final E event; - final S target; + private final S source; + private final E event; + private final S target; public static ContextTransition of(@Nullable StateContext stateContext) { if (stateContext != null) { From ceecb4e453d387fce5f6e3d31d22520cc46c8369 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Tue, 8 Oct 2024 08:06:52 +0200 Subject: [PATCH 14/16] feat: improve NameGetter (AopProxyUtils) + add 'ignoredTransitions' + make 'Connection' cComparable --- .../statemachine/plantuml/PlantUmlWriter.java | 129 +++++++-------- .../plantuml/PlantUmlWriterParameters.java | 117 +++++++++----- .../plantuml/helper/NameGetter.java | 150 +++++++++++------- .../plantuml/PlantUmlWriterTest.java | 3 +- 4 files changed, 243 insertions(+), 156 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java index 47f021c58..c84c98d23 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -191,7 +191,7 @@ public String toPlantUml( ); } - sb.append(plantUmlWriterParameters.getHiddenTransitions()); + sb.append(plantUmlWriterParameters.getAdditionalHiddenTransitions()); sb.append("\n@enduml"); @@ -362,8 +362,8 @@ private void processCompositeOrOrthogonalState( Map, List> exitToSourceStates ) { sb.append(""" - %s { - """ + %s { + """ .formatted(stateToString(indent, regionState.getId(), currentState, plantUmlWriterParameters)) .stripIndent()); @@ -386,15 +386,15 @@ public boolean test(Transition transition) { processExits(currentState, subRegion.getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); // using "--" caused problem ... how to solve this..? /* - if (i != nRegions - 1) { - // Separating regions. see "États concurrents [--, ||]" https://plantuml.com/fr/state-diagram#73b918d90b24a6c6 - sb.append(regionIndent).append("--\n"); - } + if (i != nRegions - 1) { + // Separating regions. see "Etats concurrents [--, ||]" https://plantuml.com/fr/state-diagram#73b918d90b24a6c6 + sb.append(regionIndent).append("--\n"); + } */ } sb.append(""" - %s} - """.formatted(indent)); + %s} + """.formatted(indent)); } private void processSubmachine( @@ -408,8 +408,8 @@ private void processSubmachine( Map, List> exitToSourceStates ) { sb.append(""" - %s { - """ + %s { + """ .formatted(stateToString(indent, abstractState.getId(), currentState, plantUmlWriterParameters)) .stripIndent()); final String regionIndent = indent + INDENT_INCREMENT; @@ -417,8 +417,8 @@ private void processSubmachine( processEntries(currentState, abstractState.getSubmachine().getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); processExits(currentState, abstractState.getSubmachine().getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); sb.append(""" - %s} - """.formatted(indent)); + %s} + """.formatted(indent)); } private void processSimpleState( @@ -434,8 +434,8 @@ private void processSimpleState( // TODO allow plantUmlWriterParameters to add links in description ?? // eg: state "CarWithWheel [[http://plantuml.com/state-diagram]]" as CarWithWheel sb.append(""" - %s - """ + %s + """ .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) .stripIndent()); } @@ -483,8 +483,8 @@ private void processPseudoState( case INITIAL -> { if (StateMachineUtils.isPseudoState(state, PseudoStateKind.INITIAL)) { sb.append(""" - %s - """ + %s + """ .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) .stripIndent()); } @@ -496,18 +496,18 @@ private void processPseudoState( // see historyStatesToHistoryId and historyTransitions } case ENTRY, EXIT -> sb.append(""" - %sstate %s <<%s>> - """ + %sstate %s <<%s>> + """ .formatted( indent, state.getId(), getPseudoStatePlantUmlStereotype(pseudoStateKind) ).stripIndent()); case END, CHOICE, FORK, JOIN, JUNCTION -> sb.append(""" - %s'%s <<%s>> - %sstate %s <<%s>> - %snote left of %s %s: %s - """ + %s'%s <<%s>> + %sstate %s <<%s>> + %snote left of %s %s: %s + """ .formatted( indent, state.getId(), @@ -525,7 +525,7 @@ private void processPseudoState( /** * Return a PlantUML stereotype for a given PseudoStateKind
- * we use <> stereotype for JUNCTION
+ * we use <<start>> stereotype to represent {@link PseudoStateKind#JUNCTION} in PlantUML diagram
* see UML 2 - State Machine Diagram * * @param pseudoStateKind pseudoStateKind @@ -559,8 +559,8 @@ private void processStateDescription( ) { for (E deferredEvent : state.getDeferredEvents()) { sb.append(""" - %s%s : %s /defer - """ + %s%s : %s /defer + """ .formatted( indent, state.getId(), @@ -571,8 +571,8 @@ private void processStateDescription( } for (Function, Mono> entryAction : state.getEntryActions()) { sb.append(""" - %s%s : /entry %s - """ + %s%s : /entry %s + """ .formatted( indent, state.getId(), @@ -583,8 +583,8 @@ private void processStateDescription( } for (Function, Mono> stateAction : state.getStateActions()) { sb.append(""" - %s%s : /do %s - """ + %s%s : /do %s + """ .formatted( indent, state.getId(), @@ -595,8 +595,8 @@ private void processStateDescription( } for (Function, Mono> exitAction : state.getExitActions()) { sb.append(""" - %s%s : /exit %s - """ + %s%s : /exit %s + """ .formatted( indent, state.getId(), @@ -660,10 +660,13 @@ private void processPseudoStatesTransition( ) { S source = sourceState.getId(); S target = targetState.getId(); + if (plantUmlWriterParameters.isTransitionIgnored(source, target)) { + return; + } sb.append(""" - %s%s - %s%s -%s%s-> %s %s - """ + %s%s + %s%s -%s%s-> %s %s + """ .formatted( indent, historyIdGetter == null ? "" : "'" + source + " -> " + target, // if history transition, add a comment with 'real' state names @@ -729,11 +732,11 @@ private void processTransitions( case EXTERNAL, INTERNAL, LOCAL -> { String arrowColor = plantUmlWriterParameters.getArrowColor( currentContextTransition != null - && transition.getTrigger() != null - && ( - currentContextTransition.getSource() == source - && currentContextTransition.getEvent() == transition.getTrigger().getEvent() - && currentContextTransition.getTarget() == target + && transition.getTrigger() != null + && ( + currentContextTransition.getSource() == source + && currentContextTransition.getEvent() == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target ) ); addTransition(sb, indent, source.toString(), source, target, plantUmlWriterParameters, arrowColor, transition); @@ -756,31 +759,33 @@ private void addTransition( String arrowColor, Transition transition ) { - sb.append(""" - %s%s -%s%s-> %s %s - """ - .formatted( - indent, - sourceLabel, - source == null ? "" : plantUmlWriterParameters.getDirection(source, target), - arrowColor, - target, - TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) - ) - .stripIndent() - ); - if (StringUtils.isNotBlank(transition.getName())) { + if (!plantUmlWriterParameters.isTransitionIgnored(source, target)) { sb.append(""" - %snote on link - %s %s - %send note - """ + %s%s -%s%s-> %s %s + """ .formatted( indent, - indent, - transition.getName(), - indent - )); + sourceLabel, + source == null ? "" : plantUmlWriterParameters.getDirection(source, target), + arrowColor, + target, + TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) + ) + .stripIndent() + ); + if (StringUtils.isNotBlank(transition.getName())) { + sb.append(""" + %snote on link + %s %s + %send note + """ + .formatted( + indent, + indent, + transition.getName(), + indent + )); + } } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java index ab0877c18..d0420adc2 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.TreeSet; import java.util.stream.Collectors; /** @@ -35,32 +36,32 @@ public class PlantUmlWriterParameters { private static final Log log = LogFactory.getLog(PlantUmlWriterParameters.class); public static final String DEFAULT_STATE_DIAGRAM_SETTINGS = """ - 'https://plantuml.com/state-diagram - - 'hide description area for state without description - hide empty description - - 'https://plantuml.com/fr/skinparam - 'https://plantuml-documentation.readthedocs.io/en/latest/formatting/all-skin-params.html - 'https://plantuml.com/fr/color - skinparam BackgroundColor white - skinparam DefaultFontColor black - 'skinparam DefaultFontName Impact - skinparam DefaultFontSize 14 - skinparam DefaultFontStyle Normal - skinparam NoteBackgroundColor #FEFFDD - skinparam NoteBorderColor black - - skinparam state { - ArrowColor black - BackgroundColor #F1F1F1 - BorderColor #181818 - FontColor black - ' FontName Impact - FontSize 14 - FontStyle Normal - } - """; + 'https://plantuml.com/state-diagram + + 'hide description area for state without description + hide empty description + + 'https://plantuml.com/fr/skinparam + 'https://plantuml-documentation.readthedocs.io/en/latest/formatting/all-skin-params.html + 'https://plantuml.com/fr/color + skinparam BackgroundColor white + skinparam DefaultFontColor black + 'skinparam DefaultFontName Impact + skinparam DefaultFontSize 14 + skinparam DefaultFontStyle Normal + skinparam NoteBackgroundColor #FEFFDD + skinparam NoteBorderColor black + + skinparam state { + ArrowColor black + BackgroundColor #F1F1F1 + BorderColor #181818 + FontColor black + ' FontName Impact + FontSize 14 + FontStyle Normal + } + """; @Setter private String stateDiagramSettings = DEFAULT_STATE_DIAGRAM_SETTINGS; @@ -90,9 +91,17 @@ public enum Direction { @Value @EqualsAndHashCode - private static class Connection { + private static class Connection implements Comparable> { S source; S target; + + @Override + public int compareTo(Connection o) { + int sourceComparisonResult = source.toString().compareTo(o.source.toString()); + return sourceComparisonResult == 0 + ? target.toString().compareTo(o.target.toString()) + : sourceComparisonResult; + } } /** @@ -127,8 +136,10 @@ String getDirection(S source, S target) { && arrows.containsKey(targetOnly) && !arrows.get(sourceOnly).equals(arrows.get(targetOnly)) ) { - log.warn( - String.format("Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); + log.warn(String.format( + "Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", + source, target + )); return arrows.get(targetOnly).name().toLowerCase(); } else if (arrows.containsKey(sourceOnly)) { return arrows.get(sourceOnly).name().toLowerCase(); @@ -147,15 +158,19 @@ String getDirection(S source, S target) { * exemple: * S1 -left[hidden]-> S2 */ - private final Map, Direction> hiddenTransitions = new HashMap<>(); + private final Map, Direction> additionalHiddenTransitions = new HashMap<>(); - public PlantUmlWriterParameters hiddenTransition(S source, Direction direction, S target) { - hiddenTransitions.put(new Connection<>(source, target), direction); + /** + * Add EXTRA HIDDEN transitions to align states WIHTOUT connecting them.
+ * These transitions are NOT part of the state machine! + */ + public PlantUmlWriterParameters addAdditionalHiddenTransition(S source, Direction direction, S target) { + additionalHiddenTransitions.put(new Connection<>(source, target), direction); return this; } - public String getHiddenTransitions() { - String hiddenTransitionsText = hiddenTransitions.entrySet().stream() + public String getAdditionalHiddenTransitions() { + String hiddenTransitionsText = additionalHiddenTransitions.entrySet().stream() .map(hiddenTransition -> "%s -%s[hidden]-> %s" .formatted( hiddenTransition.getKey().getSource(), @@ -169,12 +184,35 @@ public String getHiddenTransitions() { : "\n" + hiddenTransitionsText + "\n"; } + // ---------- + + /** + * Array of Connection(sourceSate, targetState) used to IGNORE EXISTING transitions.
+ * The goal is to give the ability to IGNORE some transitions on big state machine diagram to make it clearer. + * ( So, this is DIFFERENT from 'additionalHiddenTransitions', which is used to add extra 'hidden' / 'fake' transitions )
+ */ + private final TreeSet> ignoredTransitions = new TreeSet>(); + + /** + * IGNORE a transition
+ * Transition (source -> destination) will NOT be present in PlantUML diagram + */ + public PlantUmlWriterParameters ignoreTransition(S source, S target) { + ignoredTransitions.add(new Connection<>(source, target)); + return this; + } + + public boolean isTransitionIgnored(S source, S target) { + // if source is null, we always show the transition (initial state ?) + return source != null && ignoredTransitions.contains(new Connection<>(source, target)); + } + @Builder @Getter @EqualsAndHashCode static class LabelDecorator { - String prefix = ""; - String suffix = ""; + private String prefix = ""; + private String suffix = ""; String decorate(String label) { return prefix + label + suffix; @@ -216,8 +254,10 @@ public String decorateLabel( && arrowLabelDecorator.containsKey(targetOnly) && !arrowLabelDecorator.get(sourceOnly).equals(arrowLabelDecorator.get(targetOnly)) ) { - log.warn( - String.format("Two 'unary' 'arrowLabelDecorator' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); + log.warn(String.format( + "Two 'unary' 'arrowLabelDecorator' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", + source, target + )); return arrowLabelDecorator.get(targetOnly).decorate(transitionLabel); } else if (arrowLabelDecorator.containsKey(sourceOnly)) { return arrowLabelDecorator.get(sourceOnly).decorate(transitionLabel); @@ -259,6 +299,7 @@ public String getStateColor(S state, @Nullable S currentState) { return state.equals(currentState) ? currentStateColor : defaultStateColor; } + // FIXME [#FF0000] should not be hardcoded here! public String getArrowColor(boolean isCurrentTransaction) { return isCurrentTransaction ? "[#FF0000]" : ""; } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java index 824dbcea3..e6dfcbd29 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/NameGetter.java @@ -21,8 +21,10 @@ import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.BeanNameAware; import org.springframework.expression.Expression; import org.springframework.lang.Nullable; @@ -32,6 +34,7 @@ import org.springframework.statemachine.guard.SpelExpressionGuard; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class NameGetter { @@ -52,46 +55,66 @@ public class NameGetter { */ public static String getName(Object object) { String name = getSpellExpression(object); + if (name != null) { + return name; + } - if (name == null) { - Object uniqueArg = extractArg$1(object, Action.class); - if (uniqueArg != object) { - return getName(uniqueArg); + // try to 'unwrap' CGLib proxy object: + Object proxyTarget = AopProxyUtils.getSingletonTarget(object); + if(proxyTarget != null) { + name = getName(proxyTarget); + if(name != null) { + return name; } } - if (name == null) { - Object uniqueArg = extractArg$1(object, Guard.class); - if (uniqueArg != object) { - return getName(uniqueArg); + name = getBeanName(object); + if(name != null) { + return name; + } + + Object action = extractArg$1(object, Action.class); + if (action != object) { + name = getName(action); + if(name != null) { + return name; } } - if (name == null) { - name = getBeanName(object); + + Object guard = extractArg$1(object, Guard.class); + if (guard != object) { + name = getName(guard); + if(name != null) { + return name; + } } // fallback - if (name == null) { - name = object.getClass().toString(); - name = name.replace("class ", ""); - // remove trailing "/0x..." in class name - // example: "Actions$$Lambda$504/0x0000000800ec3e00" - name = RegExUtils.removeAll(name, "/0x.*"); - // remove trailing "$(0-9)" in action name - // example: "Actions$$Lambda$504" -// name = RegExUtils.removeAll(name, "\\$\\d+"); - - // only keep class name - // a.b.c.D$32/0x25764366 -> D$32 - String nameWithoutDollar = name; - int indexOfDollarSymbol = name.indexOf("$"); - if (indexOfDollarSymbol != -1) { - nameWithoutDollar = name.substring(0, indexOfDollarSymbol); - } - int lastIndexOfDot = nameWithoutDollar.lastIndexOf("."); - if (lastIndexOfDot != -1) { - name = name.substring(lastIndexOfDot + 1, name.length()); - } + return getNameUsingFallBackStrategy(object); + } + + private static String getNameUsingFallBackStrategy(Object object) { + + String name = object.getClass().toString(); + name = name.replace("class ", ""); + // remove trailing "/0x..." in class name + // example: "Actions$$Lambda$504/0x0000000800ec3e00" + name = RegExUtils.removeAll(name, "/0x.*"); + + // remove trailing "$(0-9)" in action name + // example: "Actions$$Lambda$504" + // name = RegExUtils.removeAll(name, "\\$\\d+"); + + // only keep class name + // a.b.c.D$32/0x25764366 -> D$32 + String nameWithoutDollar = name; + int indexOfDollarSymbol = name.indexOf("$"); + if (indexOfDollarSymbol != -1) { + nameWithoutDollar = name.substring(0, indexOfDollarSymbol); + } + int lastIndexOfDot = nameWithoutDollar.lastIndexOf("."); + if (lastIndexOfDot != -1) { + name = name.substring(lastIndexOfDot + 1); } return name; } @@ -110,7 +133,7 @@ private static String getSpellExpression(Object object) { // Disable "Rename this ... variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'" - // we want to the name of this method to clearly state what is does + // we want to the name of this method to clearly state what is does, i.e., "extract Arg $1" @SuppressWarnings({"squid:S100", "squid:S117"}) private static Object extractArg$1(Object actionWrappedInFunction, Class typeOfArg) { Field arg$1Field = FieldUtils.getDeclaredField(actionWrappedInFunction.getClass(), "arg$1", true); @@ -124,34 +147,53 @@ private static String getSpellExpression(Object object) { return actionWrappedInFunction; } + /** + * Try to get bean name using: + *
    + *
  • BeanNameAware interface
  • + *
  • getBeanName method
  • + *
+ * @param object the bean candidate + * @return beanName or null + */ @Nullable private static String getBeanName(Object object) { Class clazz = object.getClass(); - if (!ClassUtils.getAllInterfaces(clazz).contains(BeanNameAware.class)) { - log.error("Class " + clazz + " doesn't implements " + BeanNameAware.class + "! " + - "Make sure " + clazz + " implements BeanNameAware AND contains a 'String beanName' field." - ); - return null; + if (ClassUtils.getAllInterfaces(clazz).contains(BeanNameAware.class)) { +/* + log.error("Class " + clazz + " doesn't implement " + BeanNameAware.class + "! " + + "Make sure " + clazz + " implements BeanNameAware AND contains a 'String beanName' field." + ); +*/ + try { + // 'clazz' implements BeanNameAware .. let's try to get beanName + Field beanNameFielOfClazz = FieldUtils.getField(clazz, "beanName", true); + if (beanNameFielOfClazz == null) { + log.error("Class " + clazz + " does NOT contains a 'beanNameAware' field! " + + "Make sure " + clazz + " contains a 'String beanName' field." + ); + } else if (beanNameFielOfClazz.getType() == String.class) { + return (String) FieldUtils.readField(object, "beanName", true); + } else { + log.error("Class " + clazz + " contains a '" + beanNameFielOfClazz.getType() + " beanNameAware' field, but type should be 'String'! " + + "Make sure " + clazz + " contains a 'String beanName' field." + ); + } + } catch (IllegalAccessException ex) { + log.error("Error while accessing field!", ex); + } } + // second try using getBeanName method try { - // 'clazz' implements BeanNameAware .. let's try to get beanName - Field beanNameFielOfClazz = FieldUtils.getField(clazz, "beanName", true); - if (beanNameFielOfClazz == null) { - log.error("Class " + clazz + " does NOT contains a 'beanNameAware' field! " + - "Make sure " + clazz + " contains a 'String beanName' field." - ); - } else if (beanNameFielOfClazz.getType() == String.class) { - return (String) FieldUtils.readField(object, "beanName", true); - } else { - log.error("Class " + clazz + " contains a '" + beanNameFielOfClazz.getType() + " beanNameAware' field, but type should be 'String'! " + - "Make sure " + clazz + " contains a 'String beanName' field." - ); - } - } catch (IllegalAccessException ex) { - log.error("Error while accessing field!", ex); + return (String) MethodUtils.invokeMethod(object, true, "getBeanName"); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { +/* + log.error("Class " + clazz + " doesn't provide a getBeanName method! " + + "Make sure " + clazz + " implements BeanNameAware AND contains a 'String beanName' field." + ); +*/ + return null; } - - return null; } } diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java index 5a034e675..158f0427b 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java @@ -70,8 +70,7 @@ public static Stream plantUmlTestMethodSource() { Arguments.of("org/springframework/statemachine/uml/simple-flat", null), Arguments.of("org/springframework/statemachine/uml/simple-forkjoin", // add hidden transition to make diagram more readable - new PlantUmlWriterParameters() - .hiddenTransition("S1", RIGHT, "S2") + new PlantUmlWriterParameters().addAdditionalHiddenTransition("S1", RIGHT, "S2") ), Arguments.of("org/springframework/statemachine/uml/simple-guards", null), Arguments.of("org/springframework/statemachine/uml/simple-history-deep", null), From 42ccaa4b7a3ffe8830d6196380e5893ec5a7079d Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Wed, 6 Nov 2024 13:47:35 +0100 Subject: [PATCH 15/16] feat: add arrow length parameter --- .../statemachine/plantuml/PlantUmlWriter.java | 6 +- .../plantuml/PlantUmlWriterParameters.java | 64 ++++++++++++++++--- .../plantuml/PlantUmlWriterTest.java | 8 +-- .../uml/simple-localtransition.puml | 8 +-- 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java index c84c98d23..99dca07ba 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -665,7 +665,7 @@ private void processPseudoStatesTransition( } sb.append(""" %s%s - %s%s -%s%s-> %s %s + %s%s -%s%s%s> %s %s """ .formatted( indent, @@ -681,6 +681,7 @@ private void processPseudoStatesTransition( && currentContextTransition.getTarget() == target ) ), + plantUmlWriterParameters.getArrowLength(source, target), historyIdGetter == null ? targetState.getId() : historyIdGetter.getId(targetState), TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) ) @@ -761,13 +762,14 @@ private void addTransition( ) { if (!plantUmlWriterParameters.isTransitionIgnored(source, target)) { sb.append(""" - %s%s -%s%s-> %s %s + %s%s -%s%s%s> %s %s """ .formatted( indent, sourceLabel, source == null ? "" : plantUmlWriterParameters.getDirection(source, target), arrowColor, + plantUmlWriterParameters.getArrowLength(source, target), target, TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) ) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java index d0420adc2..5fdd506d0 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -78,6 +78,34 @@ private String getStateDiagramSettings() { : stateDiagramSettings; } + @Value + static class Arrow { + + /** + * The 'default' arrow: + *
    + *
  • Direction is {@link Direction#DOWN}
  • + *
  • Lenght is 1
  • + *
+ */ + static final Arrow DEFAULT = Arrow.of(Direction.DOWN); + + Direction direction; + int length; + + public static Arrow of(Direction direction) { + return new Arrow(direction, 1); + } + + public static Arrow of(Direction direction, int length) { + return new Arrow(direction, length); + } + + public String getLengthAsString() { + return "-".repeat(length); + } + } + /** * Direction of an arrow connecting 2 States */ @@ -107,10 +135,15 @@ public int compareTo(Connection o) { /** * Map of ( (sourceSate, targetState) -> Direction ) */ - private final Map, Direction> arrows = new HashMap<>(); + private final Map, Arrow> arrows = new HashMap<>(); + + public PlantUmlWriterParameters arrow(S source, Direction direction, S target) { + arrows.put(new Connection<>(source, target), Arrow.of(direction)); + return this; + } - public PlantUmlWriterParameters arrowDirection(S source, Direction direction, S target) { - arrows.put(new Connection<>(source, target), direction); + public PlantUmlWriterParameters arrow(S source, Direction direction, S target, int length) { + arrows.put(new Connection<>(source, target), Arrow.of(direction, length)); return this; } @@ -122,13 +155,13 @@ public PlantUmlWriterParameters arrowDirection(S source, Direction direction, * @param target target State * @return Direction.name() */ - String getDirection(S source, S target) { + private Arrow getArrow(S source, S target) { if (source == null && target == null) { throw new IllegalArgumentException("source and target state cannot both be null!"); } Connection sourceAndTarget = new Connection<>(source, target); if (arrows.containsKey(sourceAndTarget)) { - return arrows.get(sourceAndTarget).name().toLowerCase(); + return arrows.get(sourceAndTarget); } Connection sourceOnly = new Connection<>(source, null); Connection targetOnly = new Connection<>(null, target); @@ -140,14 +173,25 @@ String getDirection(S source, S target) { "Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target )); - return arrows.get(targetOnly).name().toLowerCase(); + return arrows.get(targetOnly); } else if (arrows.containsKey(sourceOnly)) { - return arrows.get(sourceOnly).name().toLowerCase(); + return arrows.get(sourceOnly); } else if (arrows.containsKey(targetOnly)) { - return arrows.get(targetOnly).name().toLowerCase(); + return arrows.get(targetOnly); } else { - return Direction.DOWN.name().toLowerCase(); + return Arrow.DEFAULT; + } + } + + String getDirection(S source, S target) { + return getArrow(source, target).getDirection().name().toLowerCase(); + } + + String getArrowLength(S source, S target) { + if(source == null) { + return Arrow.DEFAULT.getLengthAsString(); } + return getArrow(source, target).getLengthAsString(); } /** @@ -211,7 +255,9 @@ public boolean isTransitionIgnored(S source, S target) { @Getter @EqualsAndHashCode static class LabelDecorator { + @Builder.Default private String prefix = ""; + @Builder.Default private String suffix = ""; String decorate(String label) { diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java index 158f0427b..d64108bed 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java @@ -78,12 +78,10 @@ public static Stream plantUmlTestMethodSource() { Arguments.of("org/springframework/statemachine/uml/simple-history-shallow", null), Arguments.of("org/springframework/statemachine/uml/simple-junction", null), Arguments.of("org/springframework/statemachine/uml/simple-localtransition", - // add some parameters to make diagram more readable + // add some parameters to make the diagram more readable new PlantUmlWriterParameters() - .arrowDirection("S22", UP, "S2") - .arrowDirection("S21", UP, "S2") - .arrowDirection("S22", UP, "S2") - .arrowDirection("S21", UP, "S2") + .arrow("S22", UP, "S2", 2) + .arrow("S21", UP, "S2", 2) ), // It seems Spring statemachine UML parser creates duplicated transitions! // Arguments.of("org/springframework/statemachine/uml/simple-root-regions", null), diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml index 03f5e8e4a..8e65680b2 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml @@ -16,10 +16,10 @@ state S2 { [*] --> S1 S1 -down-> S2 : E1 -S21 -up-> S2 : E22 -S21 -up-> S2 : E32 -S22 -up-> S2 : E23 -S22 -up-> S2 : E33 +S21 -up--> S2 : E22 +S21 -up--> S2 : E32 +S22 -up--> S2 : E23 +S22 -up--> S2 : E33 S2 -down-> S21 : E20 S2 -down-> S21 : E30 S2 -down-> S22 : E21 From 7245eba002d66108c7b1f303e7fe5f7b6780ede2 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Thu, 7 Nov 2024 08:49:17 +0100 Subject: [PATCH 16/16] chore: keep hiddenTransitions sorted + fix getArrowLength --- .../plantuml/PlantUmlWriterParameters.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java index 5fdd506d0..0dddce144 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; import java.util.TreeSet; import java.util.stream.Collectors; @@ -188,21 +189,18 @@ String getDirection(S source, S target) { } String getArrowLength(S source, S target) { - if(source == null) { - return Arrow.DEFAULT.getLengthAsString(); - } return getArrow(source, target).getLengthAsString(); } /** - * Map of ( Connection(sourceSate, targetState) -> Direction ) + * Map of Connection(sourceSate, targetState) -> Direction * Used to add EXTRA HIDDEN arrows, which are just helping with diagram layout.
- * This is typically useful to 'force' the position of a state comparing to another, EVEN IF THESE TWO STATES AR NOT CONNECTED in the statemachine :-)
+ * This is typically useful to position a State relative to another, EVEN IF THESE TWO STATES AR NOT CONNECTED in the statemachine :-)
*

* exemple: * S1 -left[hidden]-> S2 */ - private final Map, Direction> additionalHiddenTransitions = new HashMap<>(); + private final Map, Direction> additionalHiddenTransitions = new TreeMap<>(); /** * Add EXTRA HIDDEN transitions to align states WIHTOUT connecting them.

*3>66egNk%lw)FLr-iv09C7c9Dh_4OY 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 S20 +} + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +S20 -down[#000000]-> S21 : E2 +S2 -down[#000000]-> S1 : E3 +'S1 -> SH +S1 -down[#000000]-> S2[H] : E4 +'SH -> S22 +S2[H] -down[#000000]-> S22 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.png new file mode 100644 index 0000000000000000000000000000000000000000..889f847a78590da80a31703920fc55bdd1d8c939 GIT binary patch literal 12638 zcmd^mWmME{_b$?%(nuqz3?N;il!5{Rf`WudNDk64(gI3Kmmne`pdczC4FVDpDiYEy zNDVnsXAi#5`>(amx3kV!>wI91Gxsm<*!$YozV`O9?sW|cQWjDi92|-(nyNS8^E(a> zEKVo)5(E@bISI8Dp2P3#;Kg^J6*308*w zyedzx)^4HpEb6IgJZ@Bk^=XOQdGSe4NMclP1UqoOy*ZurD!OmU0M~=++*J=5;TBts z=`(m78x+*BX@0bRIA=}FO8ah^Rd1WfR(@CC^fVIL%a?J*wDd0Rg^buk;=y-NSbT=>W+V{h{~dqwffjNocv$?pztr-|(l&pz#l z=9(KXeqJtR^sLEYwqfFvcwbBKWzRA!sZU&zzg>uLxVU29zA5xKalPFeW0lJ&A;Ih@ z_E>_by_hRaWS#Wd3>%ft8D;SD5jkI@f+LyK-LGxkOp7tN7q!6EW4qf!p-s{&*%7^a zyIGgX+=tkex`~K)citgqTW%f)M-+8MRmH&DY~`7csiF1J9v4L`fi89bMe=VXm2IP) zBJbF~(uzIn=D>$UXPHVl3JIhl90|-Y95XyMI`Fyza5OAiKMVT3z_!mbGwx!T38}sH*WKj!FbUhlVEMfBFl% zIx8$967}Rsy0~?xaB1z=T#eYA%i%F+M+WW@Y5V9x_x_;6}YwdyFOgaCCsd z{t$->y(e%(Np7VGPpwmfjMC$|CT8i^YX-yT2AIWbHuUhZ^lL_PF4qFH7)fwdqX|9m z&8X;fO!-`KfoOsdb8A<@qk8pHRRiwq^9&3^+EZ-~k z4{l6iFzK=?T9APL$KzRjQlS#Mj)Kz$#{BU*g8zTskaPXjt5+YXo}vnq;;8K7Tn35^ z`LAbAvM+TKx!TRK)0SVNK+`ia%FDV}{i-{1ds`-6SnwvI4| zqlt-$pRe!q9g^Yaw3r(=ZZyi4QDlZt>{Z^W@hsc;>C!OucPNmCN8R}L?FyF(_RQs> zw>;#bV4-dDG!HH*qR%M$es60dJaG7yAN~5ZQzXMVN_K?^FYeh;-&wrT(NPih{#W|^ zquQUz4Ld5WyBL@@8x@$27tb(Ya)mJV_V&Mj|9&q_HPrScv|Bd`)i~W6X*smHvC%{k zdZ3$uYHe*T`~6%xnLcQAuDH0ktEVTd!xV{J7oSNLvm&IF#9@=*<>i%=`&d>+ecT?p z{@JL0e|K$d_#Hn2fe;cBI(Kd**@Bm!ztp@ziIa1(J58dBE;R15LH)~@yvoYTdyE;} zykcTvlfH(A^s5wH#l>mWoa)XmTQxJQVh$Sa)_gIx8Dq>@a=&>nCxVs4fg2MLSfS)2`&zq%f7IvXNJXkr4Ws847Id(%Ipj;VfB-yH*Y4FmahFCeph`{*X?_u9s>hIfI`mWI8^WpM@-4< z*Orbm?D+)+SX-kaqGU@=P1QtqFj#jjtghDI>!4+tLWYDL?(eoV@Mb&eRiOBFvVM=( z$!qT^C@3^HH^;Ea2JdoW?nRZ);dOR(&CJX&o;Q_wF#9?AoCzTjQSKQ{rQsLe7%gZJP2BAA#n0t}sb95~6I=Y%lmtjHNjF`o)mo^cm zv$>`H8C(yZt~CbX+4g3&wx%S$fBzoP`d=GgR9rkKwWp@02HQFF_bdXRKYtz{Pg@e` zE@D>8^!I4J7J8mVMMXukO5Y{mw6`1r+cScrLj9OeU zW^+N=dmU#tX_Ef&oYu8#4>wvF%^z+Lba!|Aeo>P+I5-&65mdEV{_z@isPQ4+p1L?@ zb)tcTjcxRmegT;0_kjV|jppV{0RaIdhgzDNfq7xqbx3mJ)aw%y6NB)GV%QaLuSs5P zQf4}~AdRS_&I%!-avph4;uS$nNHQB45fON*7a2WuK(^WN`LkyKj|Kt)f}EV3w{PE8 z35Qxl2u*y+kZy;y*!6K58%|0+Y*a-Kms^*y1u+{#9J03NyBRjMw=pC05>GCpMa(h@ zkIaNxQADJqww7%-keYU5R{p_k#JhJAnL2A@HQ_(Was5%dV_uf(2)~`*H(%d|w@ndJ zAj*b^hD2_a;V7cqlfTFdU|@NpA`e+_bWY?e;~43{1m36rN@WrAIitj-r#(W>hJH5 ziHU)!Hc{Xre9-rpHs7P0KJ_%FG1_?J5t29RT_O#Q@12-9PP5Ira{YQ61#Vnws-2V5 z*bBADot2+d>kS`Nd(fyT)}!T~t(*jZZb)7V~_zwBhJaC<)!_Xm!ZX zP)i^Zi0?t#($1r0@7~F~PKSnsv?S;#-M6?)^bk)205FD?*mX+39t`Q|aKATQa(SvH zyy_a$MNU7z{efbm7SEeE8KQj3P8~Jf^4?pY@^(2h|0RQ{i<6D-(<5oj8?(BR*Cv%% z2qNS<74*im`o&ni_r`Sg@qz7%)ZXA*D-Ar`@!!84nK)f0>Z4hSX{I!k5WU00K0hm* zs#G+EgoVM{tHaB}ytfC8s>3HI>L2b`g^M7h_m+xFk%`na?6X=rI$o}>G{XwXLqo>z zqxuXJ2=1+XE1-&@<^F1ld2^?RaNcR`BfWG)PtP-X%cSUB!XRdQ6iOk6_HMrjMqFIn zBCFD8(`J;DfQE*KBj7+RhJ{yL9KBLG(WZAvL_4MQ`E#6`DQL{sHS}DUB}F9MPe3G+ z?B?d?ipNEIwiD~GO(8_0CKcq;n(fVx$iPaT=L~{xtm*7>VuP4D}_>^t0v=i(x9!2LxfCwX+m3 z5Q$s$c)ZsqgCZll^0ZQrcwC5Uaiv8?q-_c<9IToVDJg^C;K=VX7J1@s^V)2D`1vGg zA3wk28II#dz2V8JDc9N0G;ESFaQHwvziQNRTa}<7QU=VY*;=f(P?S&2rI;W6D#%8D zw`+^#5Y65d`GTK23kDaKkU*!shYZPNQt(~}jB@>MM|nmLUjqzYGb-MtJ5>zbV0`EJ z=up(WJ}OszbA761b*#p`)|*C}I=H_I1*Wa|-Ec8GTQb|M&R5cnnf>(X*_oM;+)#@P z+!Fvnf3|+Pp=Ym*aw?nNxwCEN`PtjT^Hdk5Y_Ef%Iu9BE&}gl0mp!I49?o3)*z1=| z!=cnUdDWIUR8msX`Sa(8hli_Fl7~h{ZWmzbQ5lr!-5c9zYn)Ir_ ze)L#QPiJ9s;zoRUM00_Uk3gClx9>Sh+GYHGWo7t%YUeoNFM0_(}r8$#N=m@ zp)i|dl1d0+#}1F9WR7|L<#0;DWfOJ4s{7yaDPqz)Q_TG_KTKSv1e&D(%!Sacrb{{? z4cZJ04L<uDZyWx~)Mm{*~RyG{Ie*S#&?PUeZ-(`1ON!gtCCfN$EKf6Tpf@`Z* zTlXdGlCs`=B$Qa$df~|1m$Z5+)}B*1EpFPD{a3SpZ{vDXFC3tC=joPxYBpc9{IMD@oh*f0*azV2FH@{?QUY}07`^`) zCa-T~M3fJJc+jt)`AhwL@U_f$cBo8l^#j*gD*Yf?WKi-#8}FX4Sy8z@e}-aA_B z^DVV991h>n$;m@he7LX4vch&l#+xcw<=EjnfQfm)VUpj_q89ePg!uUB)zvp+&V>@a zio2tg6aGN%O$-g^*47$;h1G^DDJUv>h$;^CHF=2HaJ;DJ!q{}D=IoQSTLtvT_6^_p zlOiuGo91Ui>1A-aZvI9vWT;^SZTdQj^h`{4vs8GVzlTosPP0$X*!K+|9`C5oToV0b zo)0*^(tJ&@QHBDKH3l8DGp>$IZ=+Cs*DpE&nWg23?PT!CgBc+8&0|KyJ+=9Ff+G-7 z{+K1nwQw~JjhRBzYWH997kSL<=7Vbpo$srrYKNekgei}v3QaKp+gtvpyU0d*r_pFM z60cu@C6UU^RbIZPyj*;gQ|pGle)hwiTWoVwd-A3q?CE1@zvQZ50tNN+OH3*$HLS1r z`eFmAYohX`!o>#rnKPHse7w9{YZDwWW@D3|kpWW<-|5ye7oB6gj2;*mXhL#tF5}(r z=;`tF@MzUD3zAp^XMLQjDuLjIDIVcI*`r`BA#^1pPc1sX<1nr z_&K@(bwx$x)b2y2pg;*1h+5G;ODxU zn`h|Ea02K0Mg$tx}O%_iM$)D=}|4f+*9h*UTFg zPZcN`zB)KEtM26VTFgAsMQO0q-0Q)E>A45Sp>NUPl)VE3+Yr{ORM^+OlZwPj-LkSojraVC86_`)?M}w)Y6*KsptWR zdPfFkX4rGO`{}T6k?*#C59>R7`7+$9eTG?*WqjW3kGR`2`eC&l8`oAEp&_C{R?m`yxf zTx6mh=~;%7@^bX6f~@!fzaH)`FNj4S)$Aq2>MVX%x^P^ui$l#T!q02tieP!u(|1QN zGF^K|y3@w$f=#pvRo6X;xMzI!S1RGnk8(r%FKE?gW@cW0CfBiY-TK3a4`sYho;w_(A)7v}?dEnunZ88QC8IL7{Tv90`r9CS3Xcj)qPk*e5I9gIWWJN=p0#yT2ctU6C?^AARb5rJ(JtH^XDSWUsHK5o*TlZKj0|~* zP~F|yrezxIoM7xfZvtqAOc6;FQ<2vpdfMCDXTGa(iHm;)1lD-0k(itu956RGSLMjE z_mqm<`*3@(VQ-pjlv4pH^5f{})+><&QM)@kJL}Wagwr&LBLaN<#b?JGSXa-#b?2iy z4Ti1&b`!x~Abh@8XDLItL`R|Tt>gmylT%9ENP9b#q?Ht~V7cV9WUW3F!@kRY6`ZW-N zrSGp~MwpQ0!1w0an@4W?4T_y(W@biqgRJRBqs9}2YHa5SAP=ao6;-;&H<2b`>$|=5 zC^uTnwuddXoA2z|U{8c=s9u$M1iDjbm$@I<4)M@wtbodHLi9HV^- z_xL!p#~|XqD=$Zes6g~(5HX&dpC@#nOwHc=^QYP8=Fjf{yzS1#_Ldua`bpsz(5E2& zEBfs~z?N{fBP@@n@?QBcSdW=!1ta820}zup043#&vOk-2>XXsg&E4JbZvrv2v2iz) zCOGH z)1Fh5X$1VN@rM>J9EwYQ6pN(&e0N$^j$m1bW)CpY^t8k$mdkw6Fl#Le3MK@nfB>;H z6GcYUY^JQ6P2Y1BnX7gd%Y4{Xxv*DNSEr$b6{VOv`o|;Hol~5WNF3{4CYB!cX9Or8 ztm8Q&aHW`K`zl?`Y3f^Fs}A{I5#d1Y{2m5yOhYQW!59M2ETR2@N6QZ_UrbBy zy6*>J7@=loS916v!p65Xpv3@T;mz9W(o%X#N;oS^*fn)ZQ2od`l-M*#9&X1+q8f_~ ziy@ED$7WP4fiMij3%i$TE|x{K6=ve`RHlzd-WR-P04!ReJoVDIA0bCJ+=2 zNR=Vsrfn_I$?kD_*P%(AX?9LT#MIPudV2c&osam!1r0~rrI5b>)_5DaJ$jv@J%@+h z+_apJ887=#Q8DlQ5Y~JSG4jE#A2+Pgb*2q)NySLsV`*!9(0t~wn;teGsnl$9$^=s0 zst$Ra+^(;AS|5Hoihspj7%s^2CG{Q3)u2L9Nm(=($TMDW8}jYAtuzV54y-pOHg-@c z^hq=F#T|?C4oyn!Ba7}Ce;2|#t$wLuuv_P0w<^$}p4j$ez8d^I2#~T;oeHNE8Xn%# zVI=>%U(v_IaybtNixa6~8&v8l0`{PDebCaBVopOi4j~q8OU>$r2ohw$PMFcBU|-M3 zx%XxDK%fOxs}qjX84{xkCzCy%oXQk<7N0%_1P3qRtX*xOExGCoP(t+X&((O>@tKIJ z=+}xhzKLjBXS<%Uv5eb%H%JS&Z{J?}W9#5hynDy4quOoZI_xe8&44N;hXV|b9CGf& zVV7%*({dCrG7!*hzNF9n{#}x^_Lx6waiT$a{K^fGFQ>LCNJ+11YWCIX@;Y9|pGFW| zIFgsYgcmRXBt<`8+h;Dx$aiZ|%zd%%tyMc@AqOBtcpGKumZ8`{>IPW>A7des*jtyZ zlPU8SId!Rpku>;6V`C$j+7y2znKt2MFAjr~GX(;o;pnL8!F!urLX6C06k!Wj%+)68 z3Di5#KYriFYs|um4dK*eOzX9^wPnuaAW_LY{Byzo!6vBej^go1pgA*NzTxg~dN=XT zyecX}udg@0kPL*N0rX*mEB%y-Xng6dz$+l=H$PxLl^MYLkuetu2za-Z9NkPNIy$}P z&)`_6S#R^!exPW1u+-{Z6iyZ&+N5uxH@h9~5K&nV(th-svBOqQQc{wOi_5s!q*5fc zIHLTiNqVu=V~qOq*jOs5F5*8sD_#DEKQ9dVK1JobZ7uePhT{KDR2mo}#_7fCeoBRi zz`5r%Qe858=~?C5+!$@!)+hA7rETp+ZR*bV&(E-dd`=}q4x09mVD7CN&vQzdSIV&0 z5zV$-x^H1<#dBLVVQuKG6%3~-2PpkB+#ZM$@#d$1f|F$e4Kp5ElBJ(J9pyeU$1KJC z^j7r)Yl#JvLj6-qrrXGM@{Fj@5ahDuJV@p9bR?x7XuTVGv@NbwXwheK4%K{?M_iY_ zZr9reUUpGali1utlF&CHO=sxdC)+RSk^ujog>J%Kz~LYW&a{w_KdfzIbrhe*`+%8_ z6pb@(E4$YbZ*nj&pb3?-T5whppwHhRRex(r_;h_qWh+U@-y-4Z6Oh+I!0zn4>X@bk z^%%_oz!%sz&^FwX z2qPMi#H>itkdK#^&-E|(guQ~&&ET6ckYj)%VfW5EKR^Ghl9JMIS5EHv8lkq_y5|fr zqz3;=`-p%4Zg;KFxRzs(WmTiUQ zy|Jq#|N9Lfj9t!MAcm9>dk5-qL_}X*-KZi#8n4Y7m5A?WUfbRq8j$pE5#B zU}#Dbd2inIw6&>sYk8i$A`#%*V{QD5#-mW|)%l-3ee(A4nJSF?I~o`ms20ia&)3sB zI=v6~cmDmAkue3xE*m{W2i|Pi8@F%U+u9B;o{7esSZoZNvj0Jnky(>o(8;WMrlzK{ z{r?EYkdk9(&4!r_q3Vl{QVYbsIA&{k5Su6dbMYB4cS!bVj)|^er~PgQgv9t!*(mI_ zo{rJk*TRnASJNT6JS^1LqknX&BPRu`{LwCMB8G{005Z%GdeXrjr&dMU*DMZ zZrr$QWyOOY+>^ojoXsb|qA)6snB2Qf*z@FRZfSY=@S&@L$lp<@7y=|U9Igud^$Ru`Vj-Hd%YL4F_gwA}C;rI?poYz0L~=Xx2Q-%37VD-|?=&$4=%u3AtZIMFr@KWp%(MAZWgo ze;#pSsRmjQk3c7-V%17H{|q37k%$MPKa_=zhYU|+x2?#!Ig|uSWv;HSYqgs_5YU$; zCyLS~Y-2!{2N8XJytxIoL;oBo)lfD>!gWx>cmG9e4DL-V=lrvRVoit?yN&@|2W;$; zavD*<%q9Tt-yeK)#!>s39Q_(U7J%UbEmC1&VWy=*nH`t;>wZ}lZur5qQB-xcKL@pWU68H*pLOpG)BtyX57RL*G11c}B`14oA>tDfKz!h!rbf#}7iMQCl_aUKcEC1VW_cN^0LhnBuWD&EH2^q{ zzO%vlLBbio-Bl1wTIC`rqXMvz?3n{0cFTP9{#$*v-vE1P=(|uv@$vB)!hKv=SU9;+ z4|G;jiCOPIRcIry3V>e1$y6h%s;a&f=*;}ly87S&8aM<}<61E65Qsx{}yKL zPn?;&B$Q&#yZm21tZGph3An=qI73~F}syPH&09k^1&9i6E6z_f_ zUh&M0L-|@H=*hMHQw^3$m=0^N_CNH6B=o~iivc7LQPDwXBPgP`DDOxRcN7r8#=%zMq@L#BXh2Ri zL+)DPI7~b5V@D*z*g`r33i3#J8I?@ISXeaB!Na%3Dw;})WgP;~&UO@ZK^<&LwS-kr zNGMpX1HyAPai%9%;TYtatKVPI4c=pl*P(J0zuNANY2>Aj@FONOW@TkvwNzJcgW%Mg zt=y{?2*ta+{CxY$(mORf!0~@oumt{l+hyJ68}FTmE`axVc@`T}D=V*^g7uJ}we!5$cd-YsWqqTy@ocX9qQlk`>=jrQ z1WNifHrr6v1n3V)!hsIlx!tTBrQ2M1rhdbJGq>s#-QyQ6CQ*jUwp zjYj{auI=^Lh05`IYJdIuWsEPFsvK~HVodOe`}RHY;G@-7_ZfmwGN_URg= zWMqejhkRS2TwJG;S*lFFU~z_Dzwv)msSAg(I5JC?8DM$D$jmG_@~0B?pbHl+K=*~$ zBPy&{J>Zv+aD$pOL~8_IaZynbNC=%5sAOzBJS0UNRouO=bai#PQh8&Yw1B)ZcCCK@ ze7Bv5hzRmCCjc-A0eb;+Nr-#^6}eLwNS7hYc@x>za`rMZ4%OWJ|G@_meQObK8k&n- zYB$1OzkUsx0ucv_b2Kki7Q4Yqst&2EgfGk+{2_wE21Aauzx7QAYJ3lCi6+XnzfMS~ zHNrMs5XwFe1$9qPPnk|>$2yj^G)Kz>E}3**Y!AWM=qNJLWyn`@Tw)lzr;DBK=)L;w zr%#^-1qBIwmUpZ}Dxb#BAa817ZYv5C5E0qh+B*H`EZg>{wA{-Nc0=K{MFb8yHk9mD@yA8v$)JFFf2 zBcd%7*g&UGQhf8Zoa;3HuC9?%#AV@KT_Nmo3FTB(4ULayZN?^0aZ`u?Q1(nB#zsuv zJy01zUweOae=cMNB)(Wsq1S{6u+eTiEIH*deUdkY%usK}XS}6vd3j^BQM0qN+slJQ`1laE2)%;&vGYUo2?}=f_EH_S z<`ZAh)+VnqGc2O1c0x`SlC_X+jv({no{J&xRKedXb zgVX^JC^3GE3*BLj+Bja{c~2Jee}aR2)DaO7Kze%qh`vDj?=Uvg9viM^cl|o0OiEJH z-14%#-;Sdbog)zuQAlv#(#(v+{ja>Mx1Rql)XN1hu8I)*H*K(Wa-y(e+C?ZTjx{x@ z)Qmyq$tvw^R|z!%&}gg22>;b^wt;g(ag@BgcKNiD8#TlJt$8o7W1FOo_%Pm}3qoX^ z=t$si5=ve7@%C<92Em%9HarYEegK!K2(Box|79ygpdUiT<3A+rP#^Y@Q$4LNpz6Yv zdYQ8`r+{*NG&4UM;!O8@xeYcC8b;&x~@&57!3m-jG({9Dk53i%6DRvP&JXRoN zgFdpr{V@)ufDOn|DcHU>L|RCm@PD+eInhN0Uk)R`a^;H80dv4b^uJbz?QJ3;V*u5) zDZK^yk)X}D=iNv?B!!9}H3yDCjlQC!^mhULXaBM`%8mjiAa9fp(+tFb?wt9PDp^l9>ZIw{uDJR8%b6&24!auR?zAFw`oYBl@Si6dxS zyB3|D&9VFVk%8mb$8AW9tK7a|k>&qB3(!V@l6ul^)+ubM4Xe9!;v%7%*ohNA#nOEm zZfyF*qlzUD8WC8Zilrt2hvncpVfg>M8&ZPjmn+- z{hY~M}+w4uz2#T*`{zyU?2`!Cye*(S!Ylm zG&phyF-shWoh(PK6rt`1do!_N0msF(Aige4l$9Zj1N#0LJpf%zaCmVoV1Or^VWrd* zdQi32l(m{c8*I72w7GBop`$=;(Y%_LqW|T@agn~#B^qoq_& S20 +} + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +S20 -down[#000000]-> S21 : E2 +S2 -down[#000000]-> S1 : E3 +'S1 -> SH +S1 -down[#000000]-> S2[H] : E4 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.png new file mode 100644 index 0000000000000000000000000000000000000000..7a935bbc3b3871498331426daea0a5c752b0a72f GIT binary patch literal 16057 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 S6V8yM-IrCqS0rup{C@y$9^{Mw literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml new file mode 100644 index 000000000..6a6d5446c --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml @@ -0,0 +1,30 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +'JUNCTION <> +state JUNCTION <> +note left of JUNCTION : JUNCTION +state S1 +state S2 +state S3 +state S4 +state S5 +state S6 +state S7 + + +[*] -[#000000]-> S1 +S2 -down[#000000]-> JUNCTION : E4 +S3 -down[#000000]-> JUNCTION : E4 +S4 -down[#000000]-> JUNCTION : E4 +S1 -down[#000000]-> S4 : E3 +S1 -down[#000000]-> S3 : E2 +S1 -down[#000000]-> S2 : E1 +JUNCTION -down[#000000]-> S7 +JUNCTION -down[#000000]-> S6 : [s6Guard] +JUNCTION -down[#000000]-> S5 : [s5Guard] + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.png new file mode 100644 index 0000000000000000000000000000000000000000..e92169656894fc0c98f9ef1de2c0d41cbd4aa10d GIT binary patch literal 26714 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml new file mode 100644 index 000000000..fb3694124 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml @@ -0,0 +1,28 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 { + state S21 + state S22 + + + [*] -[#000000]-> S21 +} + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +S22 -up[#000000]-> S2 : E33 +S21 -up[#000000]-> S2 : E32 +S22 -up[#000000]-> S2 : E23 +S21 -up[#000000]-> S2 : E22 +S2 -down[#000000]-> S21 : E20 +S2 -down[#000000]-> S22 : E21 +S2 -down[#000000]-> S21 : E30 +S2 -down[#000000]-> S22 : E31 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.png index 7424c97bcf0ea10545b05174bc5b08169bbe59be..27614af45c15ef63ff4fc181d5c276b2a80465b5 100644 GIT binary patch literal 16497 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 MBlU+x@@4`517ky`SpWb4 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml new file mode 100644 index 000000000..0013878e5 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml @@ -0,0 +1,26 @@ +@startuml +note "!!! NOT WORKING !!!\n!!! duplicated transition: 'S3 -down[#000000]-> S4 : E2' !!!\n??? Error in Spring Statemachine UML parser ???\n see [[https://github.com/spring-projects/spring-statemachine/issues/1141]] " as NOT_WORKING +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state null { + state S1 + state S2 + + + [*] -[#000000]-> S1 + S1 -down[#000000]-> S2 : E1 + state S3 + state S4 + + + [*] -[#000000]-> S3 + S3 -down[#000000]-> S4 : E2 +} + + +[*] -[#000000]-> null + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.png new file mode 100644 index 0000000000000000000000000000000000000000..38fb043336d645bee7ddb409ea1ce27b41927938 GIT binary patch literal 9732 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml new file mode 100644 index 000000000..edd3cef78 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml @@ -0,0 +1,16 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +S1 : /exit extendedState.variables.put('myvar2','myvalue2') +state S2 +S2 : /entry extendedState.variables.put('myvar1','myvalue1') + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1\n[messageHeaders.get('foo')=='bar'] + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..63f53cc8258468949a22a43adddfdbefcb9fe736 GIT binary patch literal 6161 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 S1 +S1 -down[#000000]-> S2 : E1 +S2 -down[#000000]-> S3 : E2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.png new file mode 100644 index 0000000000000000000000000000000000000000..2044e4f3dbf4db03bf5e0999533f19970dcf7d6e GIT binary patch literal 5017 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml new file mode 100644 index 000000000..4c99b381c --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml @@ -0,0 +1,21 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 { + state S11 + state S12 + + + [*] -[#000000]-> S11 +} +state S2 + + +[*] -[#000000]-> S1 +S11 -down[#000000]-> S12 : E1 +S1 -down[#000000]-> S2 : E2 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f6a9c77dcdbe50d7936ed9d889a7afdf9c6f71 GIT binary patch literal 10359 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 WvWT~8D&PblfsTfOdMV5 S30 + } + + + [*] -[#000000]-> S20 +} +state S3 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +S20 -down[#000000]-> S21 : E2 +S30 -down[#000000]-> S31 : E3 +S2 -down[#000000]-> S3 : E4 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.png new file mode 100644 index 0000000000000000000000000000000000000000..c02a1c833171be549f6ef8a130361479d5e79c4a GIT binary patch literal 11195 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml new file mode 100644 index 000000000..3a89945ab --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml @@ -0,0 +1,22 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 +state S3 +S3 : /entry s3Entry +state S4 +state S5 +S5 : /entry s5Entry + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S4 : E2 +S1 -down[#000000]-> S2 : E1 +S2 -down[#000000]-> S3 : every 1 second +S4 -down[#000000]-> S5 : after 1 second + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.png new file mode 100644 index 0000000000000000000000000000000000000000..2f19c4c4b0f5a2148663bf11a93af8369033f6ba GIT binary patch literal 5576 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 literal 0 HcmV?d00001 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml new file mode 100644 index 000000000..ea2dad72b --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml @@ -0,0 +1,16 @@ +@startuml +'https://plantuml.com/state-diagram + +'hide description area for state without description +hide empty description + +state S1 +state S2 + + +[*] -[#000000]-> S1 +S1 -down[#000000]-> S2 : E1 +S2 -down[#000000]-> S1 : E2 +S2 -down[#000000]-> S2 : E3 + +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.png new file mode 100644 index 0000000000000000000000000000000000000000..98eb0afbecf17155c9704a677041cee49245e3a9 GIT binary patch literal 6073 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 S1 +S1 -down[#000000]-> S2 : E1\n/ extendedState.variables.put('key','value') + +@enduml \ No newline at end of file From 238be9c1010f87b80d97f74f14bff2b728c697c0 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Fri, 5 Jul 2024 17:08:23 +0200 Subject: [PATCH 06/16] feat: illustrate use of hiddenTransitions --- .../statemachine/plantuml/PlantUmlWriter.java | 2 ++ .../plantuml/PlantUmlWriterParameters.java | 22 +++++++++++------- .../plantuml/PlantUmlWriterTest.java | 7 +++++- .../statemachine/uml/simple-forkjoin.png | Bin 19517 -> 20427 bytes .../statemachine/uml/simple-forkjoin.puml | 4 +++- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java index f000c05e2..1dac772c7 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -181,6 +181,8 @@ public String toPlantUml( ); } + sb.append(plantUmlWriterParameters.getHiddenTransitions()); + sb.append("\n@enduml"); log.debug("toPlantUml:" + sb); diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java index 22e5a0669..c49120f3f 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -1,7 +1,9 @@ package org.springframework.statemachine.plantuml; -import lombok.*; -import lombok.extern.slf4j.Slf4j; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Value; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -68,8 +70,8 @@ String getDirection(S source, S target) { Connection sourceOnly = new Connection<>(source, null); Connection targetOnly = new Connection<>(null, target); if (arrows.containsKey(sourceOnly) - && arrows.containsKey(targetOnly) - && !arrows.get(sourceOnly).equals(arrows.get(targetOnly)) + && arrows.containsKey(targetOnly) + && !arrows.get(sourceOnly).equals(arrows.get(targetOnly)) ) { log.warn( String.format("Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); @@ -99,14 +101,18 @@ public PlantUmlWriterParameters hiddenTransition(S source, Direction directio } public String getHiddenTransitions() { - return hiddenTransitions.entrySet().stream() + String hiddenTransitionsText = hiddenTransitions.entrySet().stream() .map(hiddenTransition -> "%s -%s[hidden]-> %s" .formatted( hiddenTransition.getKey().getSource(), - hiddenTransition.getValue().name(), + hiddenTransition.getValue().name().toLowerCase(), hiddenTransition.getKey().getTarget() )) .collect(Collectors.joining("\n")); + + return hiddenTransitionsText.isEmpty() + ? "" + : "\n" + hiddenTransitionsText + "\n"; } @Builder @@ -153,8 +159,8 @@ public String decorateLabel( Connection sourceOnly = new Connection<>(source, null); Connection targetOnly = new Connection<>(null, target); if (arrowLabelDecorator.containsKey(sourceOnly) - && arrowLabelDecorator.containsKey(targetOnly) - && !arrowLabelDecorator.get(sourceOnly).equals(arrowLabelDecorator.get(targetOnly)) + && arrowLabelDecorator.containsKey(targetOnly) + && !arrowLabelDecorator.get(sourceOnly).equals(arrowLabelDecorator.get(targetOnly)) ) { log.warn( String.format("Two 'unary' 'arrowLabelDecorator' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java index 2c3d0c0f3..594012631 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java @@ -25,6 +25,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.statemachine.plantuml.PlantUmlWriterParameters.Direction.RIGHT; import static org.springframework.statemachine.plantuml.PlantUmlWriterParameters.Direction.UP; class PlantUmlWriterTest { @@ -67,7 +68,11 @@ public static Stream plantUmlTestMethodSource() { Arguments.of("org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices", null), Arguments.of("org/springframework/statemachine/uml/simple-flat-multiple-to-end", null), Arguments.of("org/springframework/statemachine/uml/simple-flat", null), - Arguments.of("org/springframework/statemachine/uml/simple-forkjoin", null), + Arguments.of("org/springframework/statemachine/uml/simple-forkjoin", + // add hidden transition to make diagram more readable + new PlantUmlWriterParameters() + .hiddenTransition("S1", RIGHT, "S2") + ), Arguments.of("org/springframework/statemachine/uml/simple-guards", null), Arguments.of("org/springframework/statemachine/uml/simple-history-deep", null), Arguments.of("org/springframework/statemachine/uml/simple-history-default", null), diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.png b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.png index e521d87cf9943635d724ad18e9f533bd24f35f0c..1354cf651c4adfa6dec5d0e578dae3a51b99674a 100644 GIT binary patch literal 20427 zcmeIacTiN>_b-Z~prD{cC8~&kWKlt|0TF2>BT*zMNR-slq$a8$84N@vw`35JoEZU8 zKypwrjX;xAlha-8jLyvW_q$cM-mCk^tM{v#s+nod*=L^})>@yikH4xi@+bup1qlhs z(OWlVZ;vWt<2jq_v+N2o-goLsmjg@T&M8s(>Jr7 z-QQ~CvFhEtou=-o(mb5^xzEtk>{J`G34y!KnQ36|g6@w!?%w2hd44& z+C{U7$xI(A%J6&HdM#EK(U|l7&EbYazF#;_Av;dE`d?%G+K`3%ynNVB$T36s*c_k?M_-;`_2^w5=~ZH#1ETD*5A5 zPqMpLzgAJlYypN#SY#dB`fU>1w}xm*#Tp!3Ji_lu!S%}gS#k#937e1_6TgR6;muSb z(}&O2!Y`hgkW{7=sK-w8dp*{p%zn_E{CIKn0T+HPM&Z8D`0J$M(F1ZH5E7ObL${-<8a=#^eO~ct>c7W1N(QK1^ zLBqc^OSX2W^iJ=MwoL2q*%`Nc+8w43-o2rQzH2>7bLQGjMZ8w;nH&1On&%_5rrK3y z>#}<(l77f|>pl1OJ8*o@?5k($sQbAuw;nY|J2h{(OD%sf8+UK1b*W$8e$@Qn!l)xT ziL{dqj)cT(^%#O=_<$c9iQRr#Y7()1N#rCkByCv#+S=Op@89q4Y&qdu|ERC`t!#H- zN<^X5Nz`QsX?!9g_Des$@ayI;pu9g>L=+yMC%bpSkL^I*IWN50AEHBU z9OnXz(1weB`=UsakOvha1?XvS2$$xp&ypy@yTFS{Tb#ydc+m=YwL|YoR76O_7?O~z z>to{5>G06Uk#zgRD2P8?r|`OcjgUs>DM^vQl7v)p?l?l?a}065=C02^?#KAH1B0jN z6P*3nzLu1XTq8S{$a~Qjzi+se{`GYNi5;He>uC&$7}{qX#+0R0#vVqMNV9nZ8LDGo;@?Pl?!MD6vl5-f`hK$LcBHM& zhhG~%e_mK!EldvO$C$&)TU}Gy*F*u!^^d3IQVZ3j-Gp2@YW>vXOpKxjg*atH&nd@< zUGrDZ*1aST6Jj_iuK#Ndn~#0riZ9>W-9jJ`JSaRb+=SvF$&(?Nn{2JlqMwb;%_-MQ z5W?Se_!S@bx^o(Hu`^ALqi8R28h6plAO1K0!513w!NmT=9JGE6nUJt-=(DBe<-G-8 zDOEzNcBz0eY1D}%L|pL;!4C#n%@t_I7uSl+7)6Hdl6ccduNzf*T|$mXuZb z62W&z1}SbNggc+$d<2a|BKGauhil$fd_h*_#RU-&vz9piPhPKNWo2iEsto&YkVN|6d2ZX; zEtJ?#)E=>WIWuGB>FJ4L+^5HoRG+)Oy$!zy>>JhyWZ?g(K>LT#F+_;_+JIAATbt3E zgr=3%Ox50wEeg3$j}|V67xvE<+}F$d>xbvhkLAvDO1pB-aeFH@t#l+N9 zH?_aNe{P_V?ik|U3AnxCBmxEuJ8!o-b4Nbr?c2ADqxDr=b7kE;veebAvMOqDfo6ur zEhVLbfvXNvVMRqnE8AyyxOO9z1LE&T#4TqwlPEf=a7Z+Do zSHIvu>ZZb|I2}XW;4vdSFg4xW*w|QKFD@(VNmn=9USFJ?oc!nOFJ=x+T2S^CeRErgLHC8zSsp4WgyqE~?_D8`b!I}Gn?WAo06T(U8OU*XoR z<4$zdmri2>E`1NCXf=57;D=ef`?>+vyyS*#F+F@aHW}z&D>*B za_(`N>(}!>e5gf`2vNwYq^pTr_2%_0)7gtVta;Es7s z{{JJ^=iLZ>!%_GZ;&u1iTeN*jB*xbX=FJg;tJgS+AC1YwE(rWiQhiT#`n5U@0)k#> za4^3qDaq4o*$r*{_MU)OuNJ@LfM)}B1oS3!r%8F{Qq`-Jt>KyprC-=5y^j-;lOy|qp^nNi55l% zh5$~S-ydZ%WMm<65JCm3wjXU0CShM8T$X;&3l)9)_U-+9At|XUgzIY93F&9E6fP$& zVOHk`BrMwB?rg0E9z2v%0K4sm>tKHns-MhImjm2W?>YHt}bad#s{wuk~ zBmohzv9l8s_0(=b9rW!N6bV1Z$D5-?kXd|QFWojLQzu&D85_Qw(myRyBijN+-gRWN z(VghqP)w$&VW+w>C+6nnMn^@gd-I!`6sI+Gb$dU*;F#`A9~~Q8Y2eK{%f&V1sx1-_ z9IRjRC|OmRY?31S!>Zxo;XJdJ+c>BugW08a2b_iql-nFI3EW=hj!jX* zPsyn(C*n+Rg!=!!@yU}XwNEZ&p4ZHHo08IN6DRJ_(AU?8skru(j8Q=Ey@8a|T#T=; zueFE>7I*y5e#?d~=39NgASCo$xvbXTmGj)W%MZI1SuR0gLVF4mL`)80Jn$9+r4E!w zj}BD2mlfCPQ!)w=R#xbdN)T0{F2CN;&=3$1u(Y-|-(O;17eK#3dmqiv4V@2U6r>m( z_r|04%Z83v50r*0|1pDX@95ZCnO^I&t@=*=`eai5;bVwTqqDQIFJ9a)i;jrU!pa@( z`?|iqo_O7tO&tCC@_s)yE@{GvmRf&WcXxMVBO`7!4u{)MN{S3&v4~CLNPvVDFNVSm zx<;}7+1jh<=*fwRiP6!~xjBPWP&dIdp7Qbm>n$g=<>chDbqlN}jRV;LF}>b#SC9OdNiKcb;3^fiK4(GsNkHV}uI<{~ z&Xe@^iEwR$biI-fm)x`3uJ>4Yf2vc`C(mIc@o`?nA4d?5=6&!K*4&H{Lp`BMl@C83 z2H*mKSgFh?F6G_RKRbsGVB54TB)Ckar_PnlS2A6)RK-F4smy_s6Y%--5<*~{%&X*< zwINT#0?RH6`tz-eRPM>s7th%Ixb3{+rDSAeq@$w)HKPO?KRh~m9qQ!Z*5cxHo<&^S zB@Bg9gybrFl~PZgg?3FQ`At*P@gHAa-OV>289fx-!Dr1e`1JvFmlfe-Dj%P$5UCrF z`&kn^UMj~7!<8*pmxH22k62&78{l-A*5InLUz|+GTNMwF%GnY&sFTZ>3=rp#2e)f! zRs4k25A?P#95g&)aJAW=cv(wly83+CyyWJ2<>FHY2vaRp)z&g6Tj6^FH~KmN6F#6S zcMg+NSq(R=4`$Y_@?dpZlmGfgH1^6vT=o<7!tXH;bCq6S9A5iiz~WIOWB(Qc(=*tm z)0IK3Y&mQqP+rBJ;Qxs(|7@wH`Y~dHY|GSiN73JYv0a6hD*L zV#1fFg!60$O8rZcNCp(P<;JT6kro7v^rh;q7@fYal<8X!dsaUf*bY?=w0+~RlMVf- zVA=aPKE$o;%A<{!3ndJV7XwY6)zRF`Z8~x^w!(3?0gq4f_EvXs>8TsgY`W7n%IC*6 z`wwJ%?&kyYZ3aHJ*v9PKikSW2&_}VUyNlK~Yqd1!E_{5NgJEZlqycA^t60UX z_?WV0{D!RCaoXkUj~=<*A5lVauE`L>Xmi*)Hpk!C*2<~{&GC9j&<@vOFJh=z2Y-xq z6kmH5Sz4o;bXNVfM~?m6=^cx)XKeHcwQ9x95qI zC99ubR9IM;&Gna{CY1LjrN%g~M0K|(E7ZxVHIWGBag!QcmB;!D94u0q54jG2h+KfHF(^eFsMmJYEn*HM5PP;5F9wuLvMMAl9<{~amtDX*WN zjg9T(NtYj=U*zXsoG!KNR97y?C{l9iK_f6*of%`%k?dw=F8pgzzV+-gqOAfL8^&{S zN|MEb<7F$yd22p$YkGu60c+yt=NAtFxO@Fp@gkAF9CSeh2T%?MR zQTWcta$z#N=-D@0)m{88C5i2h7uIh$s`O}f6YDPYF$m{(JnVTF91@~@^XBKqMp`Fr z;gd-d^PYQUb~hya*px;hr;|St`}aWaYCjgqrP9Jge_kW)ZjURj_IS^-GBi7mzf){i zdC5pCTbG`m-eG$k6>QKmiY_TB0cf(dz1+ehJ>8SD2b0`xR2O{qQ)Ycr_Xwv?@*l)z z&7DTJ9^4DNlzq2|?XuOk?|EisUta@~s$fH4pl(y@SC&uNj6Z*T_j!_r^WrHCL9SlX zwtP_zkbtqVv2B0xHS%lLeTDApBfe1AoZE1{IjD?7O0;PD%GltzVef(^yk|Boa`Ic*(suC{{@|s$Z zO*WKHIbL$-n{nvT6GC29?@ygSpW!q=*q!<~W^0*tPn}A?$|H8yNjX-WQ@Nj>on1vr z8y4p|<)|aXd-?FirhwJZSswV-)2O*Q+UtbnjuI8p5zmIx;(G3r$&s~c5>LsFZuV5| za)!Kyd&t}Jg!rO7opT){p*BQOEApK=^TgvOgJyc9fId{28HQDr>TzVaF47{~A)CSS zjZmpwZsl)R+}7#TI&f`1rR9=>1}waL;exxJVA*m;Fy zCk@}4_*AUn>RP|Oq1&R5nB_TQvb;d#Dxjd4M4Bin28o%O!Q@~hkJe_A$6=<@k1}8O z!!`j}#v^~-u(Y|_XUn5C{`HOaHv3roRhQfG+N6t9!XnaVPtQi_4Ij9 zVeCPJtA1?hoLpQ^Gv9K7fB-B>H!+u7lLJJpGEOuTrLggRI)XK)}ZcNmvbqvG& zzZif8oT3=rUW^(YD02$54@${_*Ecprp-^;$hM_ti<)lfeMW`Tg-dBi|T}&qaJl(Kr z7phn~w!ywCrkmMP6r*qh8Y5vpu87LlA&^wUQpBbGd3g}j>QY-XdFWOf*Tx>2?|W)G z8$--~+MyQ7DJd8im$qB)#MH8Qwck0ry2f9zmx~eFM|=_O+p4NU#-D=y8lEhqx$WwSA#AStfUn&C{TH3n92l1`pRFMIgx`@Zb?Nj`gKp;R8a+vB&KQb?A;%b`! z6v1r-OiXqQlnrXHlSj6bJ~8q={PbtgpoR_UK7gFzD*?kko#J!SgN$1UO&DK z3!E7;m??R|0#foh^mKRjIi=UzKq2A0jbN@HwkBLV86vLFL|nTM14BYmlowW3`1|4y z>6bgVZ8Ee0Q0a$aSIk`yZqLW8ObO_h`eBo0LodfJ2VkFl3M6kEU4q{_%bey@0i?`) z&%?!BC@k``=}7syG}ain`M4>w-W&Fw_O7-Sq>?<%Os%@An*;ZVgLCA`?n8h9c@`b# zP)Qf9R7k(ZTzOUH@mf3dyDDB)IuL8Jv_2Z@xiwpaOG6O{=0rU}d)V5mEuOotLX2ul z+M(dBePde*ucDi{@71-&`o3RJ9q8~RuI~O-+uA@ z`Kjh`8rGH8+)$Vh$Q@(QdCT^1SXqc*un9t#@*s!YF!gdV8oAJ?xn) z8uFl>Xz z`k)Xtg#3g&F(Uv&B&N+Iv+`qNLhx?k%!n_03TY-iuhxlmi&G8XfvO6?oc zgvI0&SAe2-h!XxsW|QE^M3?bAyd~OvcyjWJY10V~#8JXzYW(VSj|z*XCdd!4Wkpb( zI>fu0d^|n@zh(OQ`Dr3!s&~tI#SWM@U_8-L(z6;FqVzA-L9_5@&(riMbDZ5=j^6{q zGLslxgjvO(o&YxPDYfK!qhOVveI2e#Jq*D5&vKYd7XawAICGV0@q3{2Q(X@ z^*Z&WJ)5kVqv!T0``Y}Fpz?`>#HXjXMYlmvbaQi~VwdD=7B0ugb0QG#pMp+0On1+A zq_%{xO4M3x5N5usx0rbX)D^o|O@iv=CJxzf6+4SEm*E~H9>BMYQ#_;;5>pevbV@CK z=C~krl#ZEMVZ#4Lg~JO5NM~|;l#7dtIPcTMo>zNva&i_x#S8z`l^b8liCg*x6dt8u zx%4aoSkS}qWSjL;yNP=e+*=zPZP(JoEDsSk&B7j}t1GZSQvA;GY@{CEMO z=gPDNa4R#q1(x9(!S5O;0rb%Q(UW7y$fZY|U}`Zt2$5f2af#aWJGaCk3a=R;m^J{c zl{!qDx-;F}tD2%ZeE8&r2YzB1Sje*i2IV>|4k+T=s=kG^b$btP0*=4WiA6Is1vb>? zCuZButULDo#i&xmQCVMI$fZ49!d=yE_`{Hp>dpvIMAiVWC{u?s^C^fG-tmt(x{T{NoF~xyFFA$W4xM zz7R{re*Sz@`~c*IKY+u~^kgz$9{(DOSd9{!I)X0jDtB3~a9s@+4^sw6xtNg*dEeXad0Q>VUWY=}6U?>Kl zjM`c9T({jwPEJ;em(z7uIEYjd`KEl31P9>7{~d+0a)`q>M5UF)1U~` zEwtLeV?#X_>ljTun9PT(AH$w{AvUD}v^7LX8l*mL5_v;&B?hME8x|P~TjCrSSD!Ub zu`u=Vlc!G=wl1o|2Ab_H$RSp3V&RVQ-icDgXQ?85e0)xyK8?Qp-euWz3pH`RuR}UN zH$FaI>^!K(^(wLAnk)|2{Mfi93=j{7D&3XdTy~$YSZi=g!TLaMmPOj3?XEc;=Q{g5 zD(a@Mv57N>^>^a_@w#1($->Wo9wh0x(P-ElbA_Y%O8BfrLOq*nR#fWa{rmU-RFgEA zfXgDzbe)=Z4P-1*`NgT+qI-Yr7jI~thz(K*R6ZQ+D%S`$w=B-(0qS4HF5NxX`>!W|#4(?e`DKurhIJ3L_9h_a!^8 z&~ftdWkG~9^xP>BJH2nZ&kM2&zd?Di8dk0=Lz8#h;*^Uj;gH~HzNZxJ262rNhaq4Q zD)LLVgWK6fgYC<)UEwFh(P^_7Sc@sjvAUfd(zg@c+~!IqW{@+CXQYT*S~WmhW$~R} z$pzF`1XX!CnuP<9;pz@wp(%sMh zV^HZWu|Oe?quP^ZA|8PEEE4j7VFH2bzACwmjZJsxjtf-f-qeQtU73#w$TEa^>#6A# zFE0|^&lStJN9#uAU8$R4a1#wZHpUWt*?_eP+qp~C;jaVRik6mk_HrkwLsn=gHEXYG z?YC85sxu#JY6SI)?U0Mm-nBAW2~&h(-d=)0uZt{o)WICKu}6;{8Mw|C6k7Lvs;m1> z#(ddo9UxV&bu#&sj-X%;Y?bCH;nC&+Cyr!R>4%s@nNk%|dp4UiVU0qnfcc#DU)5kf|{C@Y;BSyx?lX~O9 zqJy8mevQ3gLa{>aqvq&XSR8Abr8Eul>z;sq`t9U^mnyrDHanz`A3q)@8TJzPM{2j@E-C}pweF%NOmGRrCs}>};6u|< zxCauf&|w2a@{J1l2|%&VzM)LnAT`a&=>jK7bs^!*E?#9{Dpl7b-{ zwE)2%?j#}MIP=Z#I0p<95ijhWm(K)3S}RKj1?cl*F(T4Hvi`4*wJBR#PC;h&+$31! zO64Q{r6*)@RN=QU7>u;EG|UZDO4fTW4X&#*>8gqQlU3B%%}j&F`bPf>Ddyfxw(W0k zbru=j#hZZ0N;HQ!WoF^meJcBEsHOU0&9+GG^>1d$17pGNJiHGS;$5(_+^wYl6#*$b z(+H$P{=sGiu!3O^_4Lzc&;9@k z(}-8BCX7cN@@ef3Z_#LdXw^C{Fns<)<@*H7u8hJVmy-?~#EhLlm&4Yj)&s_xn^ya| z{Qdp!RC!bs2aegn`lgoJeh(rGuvCRrH6JnzD#VjRA4j(Qn!tgkXc31kpAZ_NL@y9= z3ymfK(~j?NkhD2~o>_*H;l!g2Cl>m@X67Ae-$aFdY+mD+Xsf~e$w2bgOY6hJ{2>@) z{|>AM?{o{~`_-&Zxc=^=gO9hjUxDD`WBR)vWnPq8tE@z zzqTy9CDm6^QIXxle9_!)(Pduz}>RJn;XkWvQJ>mBOOl zgt3l@?rCMizbB``HOHhe0;&@<&1vO+USZ+fJ9(zv)15IOniUyTp(kl#U+h%s=l*P` zY&pC*-@qk6_fw&7*s+^(YU#}L7?1vGeMaRyxU)3HzH$bXgIwBii3%X$(2L7G;9-t@ysnC~iVf?S|X=V)O6*)|6iIeBD-%?ST z@5y10MSqjS2i2_Z-UVa^@UT#cUL_kAPC#Rkys~oPji8Qytstyi(r)g6qJl!E8ur<& zshit&YvOfkR`CbSr&-0(eu05`22$>uXox|xW`T)X^Witc$3(5ZEdXTfo`&5Q%yj94 zfz#%a3t$~-9s|VG?Lk*sE@jZ?g8lsy-(f>2jvQgR^60HY0M)+UB3nOI(DZ4URJCZ%<`a^5!CfWmGN+HQ%L@`Ty%4brk^{g2k8qoX}MJR~J0K^6p} zCebu6R_UTeyR0*%z_eJb8v+<6NXIzTj};!u@hPvwiXvhTns1wv4{Wb78K3m`PUx&&&~Xyf~S;zHoO?yE42Fep!5Q6 z8?11x>vhU7t`7k)EB7D4Vr*ifaaCg)R~IO9;X*BI*Ry}@bA6z-iEbe(qFZPN$WymZ+;g|9mfS0O<~ zzZ*!QV!J8^Iy+HE|C-1tK6Lodq0e8w9AA@jBD?#0KR!Ho@E{Zu%WG??*S#QM1_UR? zfh?~gx$yvS^m)l#T@N|k-&c#G8Qz1q4-#}qLvcxo|MeRQ^CxL&fLUTHFsM#CNXqxC zZyfwn2-IiAvwCpvq~r0oP%-M#tj5Lt&kz4I9{cf+q`j zMlKPN<=LVkHXt#IeRz3M8xUW0-ru{nst*RpsrKZ+VxL~;MX^B5d3uzFb-A-VT*7OBXg{wxPh^Yh>ya!DCKQo|IyzBtal1eo!iLm| zQD+v6!>t~v1`XHZ`x=|me4!HnB%<7Mr1WARQ(gFwN#6_@D6>FK0vG@Sx7AaokvfiD zQ4%_+-}9}Gnhse|0ZFhj*)ahYFq}89dgV4)JPy;I%eUx|i&2FK(7*$p!Nqi|Y@E$N z>$yS9Mt!W~(W}(d(uyc6D-+AOY;cux{-hT@bc1KI!rR-s$H0|eKtMUh&Rssj)_xIJ z2h6x1)^fB?eJO6A&f?SyE&qxl(8B=a0<9EC`%D$LWHCzwB2O!0Vq$=iI6y`ga5pWO zF?19`I_$i1v$1kbjL8;kX6zS_^gZ&vq}#R*oF6db@MmJSVh>hQM$cy^zjb*3V zUXwD5A=it_d4+GDo!$GLJeNL$p?PVE$d;|7WUu|@X%4=*`@4L6d%eMhuz9KZE<^0$g}oht#?hsHa-Xx$K^ zk^i{p1%c?ld-#>;49PQ9vb;JazA4{hA>04C_`%;PRtDgR-4;7oHJD@be^<^o%>k3Z z-b(s}B|4xli(CBNpemRM_ zfX*D$Kh3$gww7}{nZli(>%_EZz7F4STcvyr$?vD*PyGw3euEuQmuR#Dd=mf;Le2vd z6MxrXjc0d$CtY06U3CIV2n!1%5_*~1Edv7s_4Q`G`4&+3 zcBkj(=YtH?ovvQr*f!XMt{JVzRNh*{%U=4@4iXa-m-T z-^=3v7|uBgPJNq*_Zp>aN(2A7XB`IOH%~7s%*?A~y0*T*eV3@$mOvbqLQ`n<4rQo3H)y z8zQ+_SD$J%6X9^NTP7G^fNr9E`tOIf^z}(_aMU=8@bhN_xmNfFw4m$0R0uTwxpi$^ z-cO)j1#Dh@D0`8CilX9=DtV{w-`ny(kXK+}Amx7py%r^YzW$wE*ZB%jkL`6JoD7TY z9$$VV?l4v19#RgCT{)r zQ1Hwn5b1#JF8l)yU2vQa|NQOmCE57RR!9Ae@!ya}zD@46W)B|tNn{-T$1_SHz}QbT`?C(8{r>&C zh-uS*gb4?Ddk`~?XvMxj4Y3Mx;J+gbLmCcTRcBWp&*AYP_p0_^nf(Vv70hQ)E_VLB z{0tc@jEM^f#$cnNUsGg!Mno!q>IM=D^E9q9j0DnnCc!IL?(}o9p0xcva7wIKo$cTR z84=IDxvqe&q&;@*`KwnPbHQ^*3V+9{Zs5xxuusg-tN4<@c>_A;WP+9F2)|!&@Xn7{ zDyB_Qi8$KKu~fT_)%gMS%i_Or6P8(=Qm5pR3HbZ_A4Oeys0BbC*dtS^n?oXJ&prU# zlVye14az#cc&ghO+BuR$Lx;@CfrwvZ8tMv%T+ks33J`w2gVPwaH{Os2-@hQ~=p&He zdclJXYN9`_Kqh30pXQhI?7z@9F9y_x&dsv3si1=Z`K`KEGz`_}pBhJTQZTcq^#DraPemg%umPU7v?T7iTP`+o8`#vvb_GFy?1$eszk}*l@y%r=76%@P3h2a% z#$Uw7JCFp`LB{(E4l^!2SyR(hS{=0>WEDpzkT$mrKsuDXd{Og4k z`YPRBK@+c4V{C&Z3&czS7MT9OUO4~b$B*DW+ab!0-UPtmGn11pv%Qy4GDg2RPwgTw z?P$B)imL$0fr{zkO_p%xzZOz`*Vy=Y_d9(FVPUnH=P>32mAjh{Q5rTs@9l&n7s7Jo zOh~C19DVVp<$?#7IWL}xwFW6rKjyj4?b|e-tR%oj!)^iErBZAP*Bpnqo2kXmxXHir zl2gIJW5Wbm0}^y}p4=P%`1X4pKG-t2v_NJtyWx6vPri_b3}B5xQ8|X>)Y8$xFVygZ zGD`tF$kP2#(bXg@mK}pXJ#`EvBMFWXkSEdE+A`n*7T-432Mw^Hj|TyP7RebA9)5|q00{zcu`US9MAOt%14vODA1MX>uuGHuCCjjSYt`-m z-QC#Il9N`IjU-6l;WMo4=NybwPXwyAGskcbELS*h#oOxY73Jk{6s_(sPun8Zn6hyE zgbxU^;!s8sCOg2CS9iFF?$|LQ#~B@z#yKGO7vJBnfhBlDP7cm5G3>cshkocoaFCU) z1@{yF5H~01jTlEBIBu<)>+WO%49Ses2J;Huhq7Ia4bI)3}9yo1sj8)2`9K8OZ;UD9Q8S&?e z18;?|*wiMx3Ke>`*8#*QT-qEB4Gjbgt_g+UhQn>7!`0Bgc(L3qxhPLnFlzOAnkxt>TV32PonlR zh9H{_NnLne3|jzvZ^h1PA1+Pp6ow>#0KBte+|@fQT|}7`&VKNQFda-HPq=yb@L|}E zjt#yBv0|ts@GJ@}ny#3@#^hE8zPYBRrcQ14;DH0h-Db2A3~+-OC^OFB)r6m6(&WFm z?v%i?ls*h48ZGc>A(oTC)I$6eS@_<~3V-mI1V&QQebW!C3o2k>$pvjS&9=_3CXXO2H=QN6Q4p z>m?9PYKdkb67D6ir+LCa;nK+P&wZ)f{`&a?_(x!nAMP+}g5Zl;_=CJojY9^m?*hpa zG;1vu%`SMl5YW7+PCu9|dH0R6NRWKW#WLa`Fu}%R8aJK^geg{?f`)(zx#~C*C6@RS zEW8l4{IO)PB)ENud&B<*r-9i@7+7a0}rx+y!MJKAWnAjTBzNtT> z=a?NN9Vl<(;Y3`F5b>sT>u>K9fnUX$U)hF8`qMsPX%0sTZ^KzR^X{zZ08c$h*wg%R zQZG-!1n|L8H5ifeArXfU5(5XQ%5{gY!%hji0f{3rQrprpLre{$rltn5i4hqL2dh-& zY%5l}gHDRnwzY%1kdF1bKUNL~h8qjV3BjfZJZ>OuG78uVu-?TZT(x3^#Gz;39p(q! zLF&bsTUZU0u7JOa0jUJ%Jyab5y75d1)(tt227B&I8{*Oq!z4Gnd;fj`tai(A)~@!j z3BW!znDVeNI@HGlFa~2wa4`4C2_b0~O;Z3eo?CrGwQBa#(CC0 zdk>~n9hOVv&;=JD)?H23Yb&?6-3km&{sN>~%}|{e;VdxMRlQ{mgir{?% zHx}f!+QTBSnA1#QKYV7$3&W?+a_J=8WC>Ve7<_cGvHZ&Y539ZQp&ieGpBJ94 zlmlkdCkH8Kkd+(b%v++}KteArEPQTi%I~WUf_JhMw;T2f4Q+wRwB|qu8Sn9kLiZ=T zG9zbG;P89(fVJFnGS- zT{wS7>6J9Wi!?&D>Z8Qr^9CtOo@EM=6N)b-(R#*Pl!wLl9@*P3udaH)!4v#AfVoMl z#?=R{(IRF*c^emsdKZCRV1K4I%>j#0cWS}e0Ol7kdB#?$!=n>K5Np!n{F06mN>V?HAO!dCtq}0~dN&AK2 zYvKWkSY{`uH85$Gf}8~;$9uUJaD9S`cuBk~0~kpVd3n(03t-keUgZ-_ynFWN-LIme zz(!6DxIF%4GGH0H>)rThVPi7Z@mBhWcxMcz5`65qSS7MHQYv#MgwHeOc&M4c`uCza zJ1fz+;K7%dP;~3&xVpMh!$RK}DrTlnB4=q8$50%Nl?0*!P~``V6Uw?e=vF4MLg zET3$;Lu*SPle8GNytTPDZ;+RQgahH^R5LE89i*z}KfagVjH41%Ppurt)D8uaOP6o? z_vwN~n!WvbR!|j9;2(Z_e;iY!Mtfr%@Uz;|g zX(n*V@#mCTgk6?SFwM7)>@T9i(fKNJ*wimk+K>ZkY{;*dK zKK=ICTSk^|Hdq_eWVzwwZ2Mj+OTd1WCOqcQR~xwsL#~4#Vj1?PF+E3^XaM zyTHXaQ{(67AS}G~^3^Ma?SdETN%j8FoDqwh%5*pX_V%YcDj(K;+E4TE3_qbxJ<8S} z*kbAQuahvHG*h;@P48I+e0)7wPL_bvyao!YZimyDA8=fpoJFoBvF44N8v}>yd^Wvt|G-HlU)=%G7c&Y{GaLrs}Iy%Kg5O$g~-pu5ufFWbn-%QH500`Gy2jj z^E=<`1g}8U2}FUV%lKIRcN>F7#Q4Y}`TaLRfxd4pS`g>&(VD2EAU5%& z$u{h(RI^~9Ks&Ri5h$GBvAJ@BOK|#Ad9ip2T4qQIaBK|~le8G1dZ1&*HCy?z~&;}C* zpXdu#GU*aHepxDJZRE$+dMk|#s;+S0aCa3h+ySBm6d!I3e5-^pRS9sC_~E0mn$BF; z1{&eevnv6A&VCcT^*tGyfl0*A^`Lrd!B9lYpHxpX6&b!0_+wH8EG=NAimwcy9S1{M ztT`Qmp!t!&!^d~kZdh7XRTa31IJc!Q#1o>#6QrP4;8>f$>nPqPfT7@2hzCTsTFJ}D zHxp^-0X#XJN=pI3K`u)@KiFCH6)+ge7lY$uuC%8nBzP%jfh)8e>>H?Ns;}*r^Es^r zo1>wSLMJ&8=)@)5xsh@J+yRuv#IT@3h@T7U9n7BuWD<)%hVW5VVu$>EQH_lgZnW)9 zjXw^r;ttvO)?^+?U*f){@NtHnVQ`Isdbb9&|JM5Y&pxj*x2~cE?Iq`bw#0z&DNR+m zfB$~Gr29<#P$I``D6pZ3D+EuPByse;Kh1^);SJD+w7bnrZ)7bh_;xNwzpoLaAB^XjdsMd2^!>-vEu+f2?73(7f_*Bj12Q^u<_w4r z#DsU1d8)5y9x#(t*%b@{4=5?n#n{)>g1K)MZMaEQ_~sm>uvj{J1enzqmYK~YzI>=M zXGPTMy_=}3_O z>Yu%~$1j{?kNkH~TaeRS06!FVW{P?y~DYzR-fG(+RrmKGIF7 zdac0b2k5itUFUB4SW@fdLgYAfnFu(oMV0pdIm!6jc3XtXsFl2@gVVh$IZzO7ue}b< z8dVN@1IYnL>p1Igu-4TZ%AeT5$KV;rS#1=3%U5b-gdHj80p|@>#HM(tWvTIRqyA2i z(+=6#3CH`}-+xqW8J+1y@@>uwHu4ekq>j04?dzy0#FsB-3R`+DV0^57dw*t#g{@_U z#5^qxpRU;`A}H1DxU4%TOdTqvlaoo0NVg#O5MP=d|2(GfaYC0*ou8p<6MD-OXs`uwi|M!{HDE7P=!7cwe)q@oE+AO^S?(>!% zi=)1Ic_N@-@z32US?pOMeh>-h7CMdz0SJg&!HcDABuDA#5hod`*YIs$I`ZUvDCnoU zSK`D_wJYVc-RY0ysCNQ!%H}mAnbACkP=9j%pn`~L@YM~%Mk(y3{4zWP!YN>v`CX5p zX9XZEI&t`@(x`2*6)`v&BU@Ov$1psnG16SmLfOgTchV~1t9m52ZYayXlezcge*ypL#h3s9 literal 19517 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 diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml index e2ab52507..1a0888ab5 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml @@ -40,4 +40,6 @@ S31 -down[#000000]-> S3 S21 -down[#000000]-> S3 S3 -down[#000000]-> SF -@enduml \ No newline at end of file +S1 -right[hidden]-> S2 + +@enduml" \ No newline at end of file From 1fca59847cbd31138e7c09d9525d483939cf8864 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Tue, 9 Jul 2024 15:48:38 +0200 Subject: [PATCH 07/16] fix: fix NPE --- .../statemachine/plantuml/PlantUmlWriter.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java index 1dac772c7..e8a170a89 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -1,7 +1,6 @@ package org.springframework.statemachine.plantuml; import jakarta.validation.constraints.NotNull; -import lombok.extern.slf4j.Slf4j; import net.sourceforge.plantuml.SourceStringReader; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; @@ -246,7 +245,7 @@ private Boolean isEntryOrExit(State state) { return Boolean.FALSE; } return PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()) - || PseudoStateKind.EXIT.equals(state.getPseudoState().getKind()); + || PseudoStateKind.EXIT.equals(state.getPseudoState().getKind()); } // TODO create processState() with optional '{' and '}' to allow text on stereotype states? @@ -662,10 +661,10 @@ private void processPseudoStatesTransition( plantUmlWriterParameters.getDirection(source, target), plantUmlWriterParameters.getArrowColor( currentContextTransition != null - && ( + && ( currentContextTransition.getSource() == source - // && currentContextTransition.event == transition.getTrigger().getEvent() - && currentContextTransition.getTarget() == target + // && currentContextTransition.event == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target ) ), historyIdGetter == null ? targetState.getId() : historyIdGetter.getId(targetState), @@ -719,10 +718,11 @@ private void processTransitions( case EXTERNAL, INTERNAL, LOCAL -> { String arrowColor = plantUmlWriterParameters.getArrowColor( currentContextTransition != null - && ( - currentContextTransition.getSource() == source - && currentContextTransition.getEvent() == transition.getTrigger().getEvent() - && currentContextTransition.getTarget() == target + && transition.getTrigger() != null + && ( + currentContextTransition.getSource() == source + && currentContextTransition.getEvent() == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target ) ); addTransition(sb, indent, source.toString(), source, target, plantUmlWriterParameters, arrowColor, transition); @@ -782,7 +782,7 @@ private boolean isHistoryState(@NotNull State state) { return Boolean.FALSE; } return PseudoStateKind.HISTORY_SHALLOW.equals(state.getPseudoState().getKind()) - || PseudoStateKind.HISTORY_DEEP.equals(state.getPseudoState().getKind()); + || PseudoStateKind.HISTORY_DEEP.equals(state.getPseudoState().getKind()); } } From 5f1a4f4eb3b6345c0034403d930514ab59df7658 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Wed, 10 Jul 2024 09:20:17 +0200 Subject: [PATCH 08/16] fix: Fix TransitionComparator --- .../plantuml/helper/TransitionComparator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java index b8d0060dd..484711b4e 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/helper/TransitionComparator.java @@ -8,6 +8,12 @@ public class TransitionComparator implements Comparator> @Override public int compare(Transition transition1, Transition transition2) { - return transition1.getSource().toString().compareTo(transition2.getTarget().toString()); + // First compare by source + int compareBySource = transition1.getSource().toString().compareTo(transition2.getSource().toString()); + if (compareBySource != 0) { + return compareBySource; + } + // If sources are equal, then compare by target + return transition1.getTarget().toString().compareTo(transition2.getTarget().toString()); } } From a7b2add356a39c42cfa51f69e4c22980a9d5cfda Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Wed, 10 Jul 2024 10:04:51 +0200 Subject: [PATCH 09/16] fix: Fix TransitionComparator --- .../statemachine/plantuml/ContextTransition.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java index 4c48cc6dc..455c08f7b 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java @@ -15,7 +15,9 @@ public class ContextTransition { public static ContextTransition of(@Nullable StateContext stateContext) { if (stateContext != null) { return new ContextTransition<>( - stateContext.getSource().getId(), + stateContext.getSource() != null + ? stateContext.getSource().getId() + : null, stateContext.getEvent(), stateContext.getTarget() != null ? stateContext.getTarget().getId() From 961f5ce39dc075fdf7f2841dd9d0f6579da9677b Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Thu, 11 Jul 2024 12:39:56 +0200 Subject: [PATCH 10/16] chore: code cleaning --- .../statemachine/plantuml/StateMachineHelper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java index 5b06b798b..4410b4c83 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/StateMachineHelper.java @@ -43,6 +43,7 @@ private static void collectCurrentStates( if (region.getState() != null) { currentStateAccumulator.add(region.getState().getId()); } + region.getStates().forEach(state -> { if (state.isSubmachineState()) { if (state instanceof AbstractState abstractState) { @@ -93,8 +94,8 @@ private static void collectHistoryState( ) { if (state.getPseudoState() != null && ( - state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP - || state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW + state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP + || state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW ) ) { historyStatesToHistoryId.put(state, historyId(parentState, state.getPseudoState().getKind())); From 59df160f001877d59b32c24128f4b1a0f8dffa96 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Thu, 11 Jul 2024 13:35:54 +0200 Subject: [PATCH 11/16] feat: add global diagram settings + update arrow colors + update unit tests resources accordingly --- .../statemachine/plantuml/PlantUmlWriter.java | 17 +++---- .../plantuml/PlantUmlWriterParameters.java | 50 ++++++++++++++++--- .../plantuml/PlantUmlWriterTest.java | 9 ++++ .../uml/action-with-transition-choice.puml | 16 +++--- .../uml/action-with-transition-junction.puml | 16 +++--- .../uml/broken-model-shadowentries.puml | 4 +- .../statemachine/uml/initial-actions.puml | 4 +- .../statemachine/uml/missingname-choice.puml | 10 ++-- .../statemachine/uml/multijoin-forkjoin.puml | 24 ++++----- .../uml/pseudostate-in-submachine.puml | 8 +-- .../uml/pseudostate-in-submachineref.puml | 8 +-- .../statemachine/uml/simple-actions.puml | 4 +- .../statemachine/uml/simple-choice.puml | 10 ++-- .../uml/simple-connectionpointref.puml | 18 +++---- .../statemachine/uml/simple-entryexit.puml | 16 +++--- .../statemachine/uml/simple-eventdefer.puml | 6 +-- .../statemachine/uml/simple-flat-end.puml | 6 +-- ...imple-flat-multiple-to-end-viachoices.puml | 10 ++-- .../uml/simple-flat-multiple-to-end.puml | 6 +-- .../statemachine/uml/simple-flat.puml | 4 +- .../statemachine/uml/simple-forkjoin.puml | 24 ++++----- .../statemachine/uml/simple-guards.puml | 8 +-- .../statemachine/uml/simple-history-deep.puml | 14 +++--- .../uml/simple-history-default.puml | 14 +++--- .../uml/simple-history-shallow.puml | 12 ++--- .../statemachine/uml/simple-junction.puml | 20 ++++---- .../uml/simple-localtransition.puml | 22 ++++---- .../statemachine/uml/simple-root-regions.puml | 12 ++--- .../statemachine/uml/simple-spels.puml | 4 +- .../uml/simple-state-actions.puml | 6 +-- .../statemachine/uml/simple-submachine.puml | 8 +-- .../uml/simple-submachineref.puml | 14 +++--- .../statemachine/uml/simple-timers.puml | 10 ++-- .../uml/simple-transitiontypes.puml | 8 +-- .../uml/transition-effect-spel.puml | 4 +- 35 files changed, 234 insertions(+), 192 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java index e8a170a89..52239c60c 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -153,15 +153,10 @@ public String toPlantUml( historyStatesToHistoryId = StateMachineHelper.collectHistoryStates(stateMachine); historyTransitions = new ArrayList<>(); - StringBuilder sb = new StringBuilder(); - sb.append(""" - @startuml - 'https://plantuml.com/state-diagram - - 'hide description area for state without description - hide empty description - - """); + StringBuilder sb = new StringBuilder("@startuml\n") + .append(PlantUmlWriterParameters.getStateDiagramSettings(plantUmlWriterParameters)) + .append("\n"); + // 2nd pass: processing statemachine AND collecting history transitions in 'historyTransitions processRegion(stateMachine, stateContext, plantUmlWriterParameters, sb, "", null); @@ -651,7 +646,7 @@ private void processPseudoStatesTransition( S target = targetState.getId(); sb.append(""" %s%s - %s%s -%s[%s]-> %s %s + %s%s -%s%s-> %s %s """ .formatted( indent, @@ -746,7 +741,7 @@ private void addTransition( Transition transition ) { sb.append(""" - %s%s -%s[%s]-> %s %s + %s%s -%s%s-> %s %s """ .formatted( indent, diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java index c49120f3f..35080327f 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -1,9 +1,6 @@ package org.springframework.statemachine.plantuml; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Value; +import lombok.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -21,7 +18,48 @@ public class PlantUmlWriterParameters { private static final Log log = LogFactory.getLog(PlantUmlWriterParameters.class); - // TODO add hidden arrows 'S1 -[hidden]-> S2' + public static final String DEFAULT_STATE_DIAGRAM_SETTINGS = """ + 'https://plantuml.com/state-diagram + + 'hide description area for state without description + hide empty description + + 'https://plantuml.com/fr/skinparam + 'https://plantuml-documentation.readthedocs.io/en/latest/formatting/all-skin-params.html + 'https://plantuml.com/fr/color + skinparam BackgroundColor white + skinparam DefaultFontColor black + 'skinparam DefaultFontName Impact + skinparam DefaultFontSize 14 + skinparam DefaultFontStyle Normal + skinparam NoteBackgroundColor #FEFFDD + skinparam NoteBorderColor black + + skinparam state { + ArrowColor black + BackgroundColor #F1F1F1 + BorderColor #181818 + FontColor black + ' FontName Impact + FontSize 14 + FontStyle Normal + } + """; + + @Setter + private String stateDiagramSettings = DEFAULT_STATE_DIAGRAM_SETTINGS; + + public static String getStateDiagramSettings(@Nullable PlantUmlWriterParameters plantUmlWriterParameters) { + return plantUmlWriterParameters == null + ? DEFAULT_STATE_DIAGRAM_SETTINGS + : plantUmlWriterParameters.getStateDiagramSettings(); + } + + private String getStateDiagramSettings() { + return stateDiagramSettings == null + ? "" + : stateDiagramSettings; + } /** * Direction of an arrow connecting 2 States @@ -206,7 +244,7 @@ public String getStateColor(S state, @Nullable S currentState) { } public String getArrowColor(boolean isCurrentTransaction) { - return isCurrentTransaction ? "#FF0000" : "#000000"; + return isCurrentTransaction ? "[#FF0000]" : ""; } } diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java index 594012631..5a034e675 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/plantuml/PlantUmlWriterTest.java @@ -110,6 +110,15 @@ void plantUmlTest(String resourcePath, PlantUmlWriterParameters plantUml // make umlStateMachineModelFactory aware of beans available in BeansForUmlFiles.class umlStateMachineModelFactory.setBeanFactory(context); + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + }// setting some PlantUml parameters + plantUmlWriterParameters.setStateDiagramSettings(""" + 'https://plantuml.com/state-diagram + + 'hide description area for state without description + hide empty description + """); // Dumping statemachine to PlantUml diagram String stateMachineAsPlantUML = new PlantUmlWriter() .toPlantUml( diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.puml index c2f7d2306..7bd8bbf57 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-choice.puml @@ -18,13 +18,13 @@ state S5 state S6 -[*] -[#000000]-> S1 -S1 -down[#000000]-> CHOICE1 : E1\n/ s1ToChoice -CHOICE1 -down[#000000]-> CHOICE2 : [choice2Guard]\n/ choice1ToChoice2 -CHOICE2 -down[#000000]-> S6 : / choiceToS6 -CHOICE2 -down[#000000]-> S5 : [s5Guard]\n/ choiceToS5 -CHOICE1 -down[#000000]-> S4 : / choiceToS4 -CHOICE1 -down[#000000]-> S3 : [s3Guard] -CHOICE1 -down[#000000]-> S2 : [s2Guard]\n/ choiceToS2 +[*] --> S1 +CHOICE1 -down-> CHOICE2 : [choice2Guard]\n/ choice1ToChoice2 +CHOICE1 -down-> S2 : [s2Guard]\n/ choiceToS2 +CHOICE1 -down-> S3 : [s3Guard] +CHOICE1 -down-> S4 : / choiceToS4 +CHOICE2 -down-> S5 : [s5Guard]\n/ choiceToS5 +CHOICE2 -down-> S6 : / choiceToS6 +S1 -down-> CHOICE1 : E1\n/ s1ToChoice @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml index 4df00132b..07b2c50df 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/action-with-transition-junction.puml @@ -18,13 +18,13 @@ state S5 state S6 -[*] -[#000000]-> S1 -S1 -down[#000000]-> JUNCTION1 : E1\n/ s1ToChoice -JUNCTION1 -down[#000000]-> JUNCTION2 : [choice2Guard]\n/ choice1ToChoice2 -JUNCTION2 -down[#000000]-> S6 : / choiceToS6 -JUNCTION2 -down[#000000]-> S5 : [s5Guard]\n/ choiceToS5 -JUNCTION1 -down[#000000]-> S4 : / choiceToS4 -JUNCTION1 -down[#000000]-> S3 : [s3Guard] -JUNCTION1 -down[#000000]-> S2 : [s2Guard]\n/ choiceToS2 +[*] --> S1 +JUNCTION1 -down-> JUNCTION2 : [choice2Guard]\n/ choice1ToChoice2 +JUNCTION1 -down-> S2 : [s2Guard]\n/ choiceToS2 +JUNCTION1 -down-> S3 : [s3Guard] +JUNCTION1 -down-> S4 : / choiceToS4 +JUNCTION2 -down-> S5 : [s5Guard]\n/ choiceToS5 +JUNCTION2 -down-> S6 : / choiceToS6 +S1 -down-> JUNCTION1 : E1\n/ s1ToChoice @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml index 4f2152f02..3c7b66d70 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/broken-model-shadowentries.puml @@ -8,7 +8,7 @@ state S1 state S2 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 +[*] --> S1 +S1 -down-> S2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml index 31f6506fb..d03426204 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/initial-actions.puml @@ -8,7 +8,7 @@ state S1 state S2 -[*] -[#000000]-> S1 : / initialAction -S1 -down[#000000]-> S2 : E1 +[*] --> S1 : / initialAction +S1 -down-> S2 : E1 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml index 4cfa8f179..302703f77 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/missingname-choice.puml @@ -13,10 +13,10 @@ state choice1 <> note left of choice1 : choice1 -[*] -[#000000]-> S1 -S1 -down[#000000]-> choice1 : E1 -choice1 -down[#000000]-> S2 : [s2Guard] -choice1 -down[#000000]-> S3 : [s3Guard] -choice1 -down[#000000]-> S4 +[*] --> S1 +S1 -down-> choice1 : E1 +choice1 -down-> S2 : [s2Guard] +choice1 -down-> S3 : [s3Guard] +choice1 -down-> S4 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml index b0e0478a2..020fe4fd2 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/multijoin-forkjoin.puml @@ -12,12 +12,12 @@ state S2 { state S21 - [*] -[#000000]-> S20 + [*] --> S20 state S30 state S31 - [*] -[#000000]-> S30 + [*] --> S30 } 'S3 <> state S3 <> @@ -29,17 +29,17 @@ note left of SF : SF state SI -S1 -down[#000000]-> S20 +S1 -down-> S20 -S1 -down[#000000]-> S30 +S1 -down-> S30 -[*] -[#000000]-> SI -S20 -down[#000000]-> S21 : E2 -S30 -down[#000000]-> S31 : E3 -SI -down[#000000]-> S1 : E1 -S31 -down[#000000]-> S3 -S21 -down[#000000]-> S3 -S3 -down[#000000]-> S4 : [extendedState.variables.isEmpty()] -S3 -down[#000000]-> SF : [!extendedState.variables.isEmpty()] +[*] --> SI +S20 -down-> S21 : E2 +S21 -down-> S3 +S30 -down-> S31 : E3 +S31 -down-> S3 +S3 -down-> S4 : [extendedState.variables.isEmpty()] +S3 -down-> SF : [!extendedState.variables.isEmpty()] +SI -down-> S1 : E1 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml index 22022b860..ea4a70fe6 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachine.puml @@ -12,12 +12,12 @@ state S1 { state S12 - [*] -[#000000]-> S11 + [*] --> S11 } -[*] -[#000000]-> S1 -S11 -down[#000000]-> CHOICE -CHOICE -down[#000000]-> S12 +[*] --> S1 +CHOICE -down-> S12 +S11 -down-> CHOICE @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml index 22022b860..ea4a70fe6 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/pseudostate-in-submachineref.puml @@ -12,12 +12,12 @@ state S1 { state S12 - [*] -[#000000]-> S11 + [*] --> S11 } -[*] -[#000000]-> S1 -S11 -down[#000000]-> CHOICE -CHOICE -down[#000000]-> S12 +[*] --> S1 +CHOICE -down-> S12 +S11 -down-> CHOICE @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml index 1a5580216..47782d860 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-actions.puml @@ -11,7 +11,7 @@ S2 : /entry s2Entry S2 : /do extendedState.variables.put('hellos2do','hellos2dovalue') -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1\n/ e1Action +[*] --> S1 +S1 -down-> S2 : E1\n/ e1Action @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml index 67c984de7..97800d2c5 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-choice.puml @@ -13,10 +13,10 @@ state S3 state S4 -[*] -[#000000]-> S1 -S1 -down[#000000]-> CHOICE : E1 -CHOICE -down[#000000]-> S4 -CHOICE -down[#000000]-> S3 : [s3Guard] -CHOICE -down[#000000]-> S2 : [s2Guard] +[*] --> S1 +CHOICE -down-> S2 : [s2Guard] +CHOICE -down-> S3 : [s3Guard] +CHOICE -down-> S4 +S1 -down-> CHOICE : E1 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml index 80adcdbe2..1cef76e1b 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-connectionpointref.puml @@ -1,5 +1,5 @@ @startuml -note "!!! NOT WORKING !!!\n!!! Missing 'EXIT -down[#000000]-> S4' transition !!!\n It seems ConnectionPointRef is not supported by Spring Statemachine" as NOT_WORKING +note "!!! NOT WORKING !!!\n!!! Missing 'EXIT -down-> S4' transition !!!\n It seems ConnectionPointRef is not supported by Spring Statemachine" as NOT_WORKING 'https://plantuml.com/state-diagram 'hide description area for state without description @@ -11,7 +11,7 @@ state S2 { state S22 - [*] -[#000000]-> S21 + [*] --> S21 state ENTRY <> state EXIT <> } @@ -19,12 +19,12 @@ state S3 state S4 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -ENTRY -down[#000000]-> S22 -S22 -down[#000000]-> EXIT : E4 -S1 -down[#000000]-> ENTRY : E3 -S2 -down[#000000]-> S3 : E2 -EXIT -down[#000000]-> S4 +[*] --> S1 +S1 -down-> S2 : E1 +ENTRY -down-> S22 +S22 -down-> EXIT : E4 +S1 -down-> ENTRY : E3 +S2 -down-> S3 : E2 +EXIT -down-> S4 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml index cbdb6cbd4..72827c936 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-entryexit.puml @@ -10,7 +10,7 @@ state S2 { state S22 - [*] -[#000000]-> S21 + [*] --> S21 state ENTRY <> state EXIT <> } @@ -18,12 +18,12 @@ state S3 state S4 -[*] -[#000000]-> S1 -ENTRY -down[#000000]-> S22 -S22 -down[#000000]-> EXIT : E4 -S1 -down[#000000]-> ENTRY : E3 -EXIT -down[#000000]-> S4 -S1 -down[#000000]-> S2 : E1 -S2 -down[#000000]-> S3 : E2 +[*] --> S1 +ENTRY -down-> S22 +EXIT -down-> S4 +S1 -down-> ENTRY : E3 +S1 -down-> S2 : E1 +S22 -down-> EXIT : E4 +S2 -down-> S3 : E2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.puml index a0f9187a5..83516f90f 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-eventdefer.puml @@ -10,8 +10,8 @@ state S2 state S3 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S2 -down[#000000]-> S3 : E2 +[*] --> S1 +S1 -down-> S2 : E1 +S2 -down-> S3 : E2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml index a83a59393..ce7292993 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-end.puml @@ -11,8 +11,8 @@ state S3 <> note left of S3 : S3 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S2 -down[#000000]-> S3 : E2 +[*] --> S1 +S1 -down-> S2 : E1 +S2 -down-> S3 : E2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.puml index 62de460d5..1824ab23f 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end-viachoices.puml @@ -16,10 +16,10 @@ note left of FINAL : FINAL state S1 -[*] -[#000000]-> S1 -S1 -down[#000000]-> CHOICE1 -CHOICE2 -down[#000000]-> FINAL -CHOICE1 -down[#000000]-> FINAL -S1 -down[#000000]-> CHOICE2 +[*] --> S1 +CHOICE1 -down-> FINAL +CHOICE2 -down-> FINAL +S1 -down-> CHOICE1 +S1 -down-> CHOICE2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml index 00f8ce967..250ebe680 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat-multiple-to-end.puml @@ -13,8 +13,8 @@ note left of FINAL2 : FINAL2 state S1 -[*] -[#000000]-> S1 -S1 -down[#000000]-> FINAL1 -S1 -down[#000000]-> FINAL2 +[*] --> S1 +S1 -down-> FINAL1 +S1 -down-> FINAL2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml index a2de77e7c..72c0afa54 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-flat.puml @@ -9,7 +9,7 @@ state S2 S2 : /entry action1 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 +[*] --> S1 +S1 -down-> S2 : E1 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml index 1a0888ab5..7e869d361 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-forkjoin.puml @@ -12,12 +12,12 @@ state S2 { state S21 - [*] -[#000000]-> S20 + [*] --> S20 state S30 state S31 - [*] -[#000000]-> S30 + [*] --> S30 } 'S3 <> state S3 <> @@ -28,18 +28,18 @@ note left of SF : SF state SI -S1 -down[#000000]-> S20 +S1 -down-> S20 -S1 -down[#000000]-> S30 +S1 -down-> S30 -[*] -[#000000]-> SI -S20 -down[#000000]-> S21 : E2 -S30 -down[#000000]-> S31 : E3 -SI -down[#000000]-> S1 : E1 -S31 -down[#000000]-> S3 -S21 -down[#000000]-> S3 -S3 -down[#000000]-> SF +[*] --> SI +S20 -down-> S21 : E2 +S21 -down-> S3 +S30 -down-> S31 : E3 +S31 -down-> S3 +S3 -down-> SF +SI -down-> S1 : E1 S1 -right[hidden]-> S2 -@enduml" \ No newline at end of file +@enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.puml index 59446d0dc..097375baf 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-guards.puml @@ -10,9 +10,9 @@ state S3 state S4 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S3 : E2 -S1 -down[#000000]-> S2 : E1\n[denyGuard] -S3 -down[#000000]-> S4 : [denyGuard] +[*] --> S1 +S1 -down-> S2 : E1\n[denyGuard] +S1 -down-> S3 : E2 +S3 -down-> S4 : [denyGuard] @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.puml index d3f3f1a7e..2c4fa7aed 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-deep.puml @@ -12,19 +12,19 @@ state S2 { state S212 - [*] -[#000000]-> S211 + [*] --> S211 } - [*] -[#000000]-> S20 + [*] --> S20 } -[*] -[#000000]-> S1 -S211 -down[#000000]-> S212 : E2 -S212 -down[#000000]-> S1 : E3 -S1 -down[#000000]-> S211 : E1 +[*] --> S1 +S1 -down-> S211 : E1 +S211 -down-> S212 : E2 +S212 -down-> S1 : E3 'S1 -> SH -S1 -down[#000000]-> S2[H*] : E4 +S1 -down-> S2[H*] : E4 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.puml index b32597bc6..3721154a7 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-default.puml @@ -11,17 +11,17 @@ state S2 { state S22 - [*] -[#000000]-> S20 + [*] --> S20 } -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S20 -down[#000000]-> S21 : E2 -S2 -down[#000000]-> S1 : E3 +[*] --> S1 +S1 -down-> S2 : E1 +S20 -down-> S21 : E2 +S2 -down-> S1 : E3 'S1 -> SH -S1 -down[#000000]-> S2[H] : E4 +S1 -down-> S2[H] : E4 'SH -> S22 -S2[H] -down[#000000]-> S22 +S2[H] -down-> S22 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.puml index fc9ebe9c3..8f4901603 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-history-shallow.puml @@ -10,15 +10,15 @@ state S2 { state S21 - [*] -[#000000]-> S20 + [*] --> S20 } -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S20 -down[#000000]-> S21 : E2 -S2 -down[#000000]-> S1 : E3 +[*] --> S1 +S1 -down-> S2 : E1 +S20 -down-> S21 : E2 +S2 -down-> S1 : E3 'S1 -> SH -S1 -down[#000000]-> S2[H] : E4 +S1 -down-> S2[H] : E4 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml index 6a6d5446c..ba3e5c3f1 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-junction.puml @@ -16,15 +16,15 @@ state S6 state S7 -[*] -[#000000]-> S1 -S2 -down[#000000]-> JUNCTION : E4 -S3 -down[#000000]-> JUNCTION : E4 -S4 -down[#000000]-> JUNCTION : E4 -S1 -down[#000000]-> S4 : E3 -S1 -down[#000000]-> S3 : E2 -S1 -down[#000000]-> S2 : E1 -JUNCTION -down[#000000]-> S7 -JUNCTION -down[#000000]-> S6 : [s6Guard] -JUNCTION -down[#000000]-> S5 : [s5Guard] +[*] --> S1 +JUNCTION -down-> S5 : [s5Guard] +JUNCTION -down-> S6 : [s6Guard] +JUNCTION -down-> S7 +S1 -down-> S2 : E1 +S1 -down-> S3 : E2 +S1 -down-> S4 : E3 +S2 -down-> JUNCTION : E4 +S3 -down-> JUNCTION : E4 +S4 -down-> JUNCTION : E4 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml index fb3694124..03f5e8e4a 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.puml @@ -10,19 +10,19 @@ state S2 { state S22 - [*] -[#000000]-> S21 + [*] --> S21 } -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S22 -up[#000000]-> S2 : E33 -S21 -up[#000000]-> S2 : E32 -S22 -up[#000000]-> S2 : E23 -S21 -up[#000000]-> S2 : E22 -S2 -down[#000000]-> S21 : E20 -S2 -down[#000000]-> S22 : E21 -S2 -down[#000000]-> S21 : E30 -S2 -down[#000000]-> S22 : E31 +[*] --> S1 +S1 -down-> S2 : E1 +S21 -up-> S2 : E22 +S21 -up-> S2 : E32 +S22 -up-> S2 : E23 +S22 -up-> S2 : E33 +S2 -down-> S21 : E20 +S2 -down-> S21 : E30 +S2 -down-> S22 : E21 +S2 -down-> S22 : E31 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml index 0013878e5..e86b3f44e 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-root-regions.puml @@ -1,5 +1,5 @@ @startuml -note "!!! NOT WORKING !!!\n!!! duplicated transition: 'S3 -down[#000000]-> S4 : E2' !!!\n??? Error in Spring Statemachine UML parser ???\n see [[https://github.com/spring-projects/spring-statemachine/issues/1141]] " as NOT_WORKING +note "!!! NOT WORKING !!!\n!!! duplicated transition: 'S3 -down-> S4 : E2' !!!\n??? Error in Spring Statemachine UML parser ???\n see [[https://github.com/spring-projects/spring-statemachine/issues/1141]] " as NOT_WORKING 'https://plantuml.com/state-diagram 'hide description area for state without description @@ -10,17 +10,17 @@ state null { state S2 - [*] -[#000000]-> S1 - S1 -down[#000000]-> S2 : E1 + [*] --> S1 + S1 -down-> S2 : E1 state S3 state S4 - [*] -[#000000]-> S3 - S3 -down[#000000]-> S4 : E2 + [*] --> S3 + S3 -down-> S4 : E2 } -[*] -[#000000]-> null +[*] --> null @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml index edd3cef78..cd81a9c3e 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-spels.puml @@ -10,7 +10,7 @@ state S2 S2 : /entry extendedState.variables.put('myvar1','myvalue1') -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1\n[messageHeaders.get('foo')=='bar'] +[*] --> S1 +S1 -down-> S2 : E1\n[messageHeaders.get('foo')=='bar'] @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.puml index e54302810..30e3740f7 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.puml @@ -11,8 +11,8 @@ S2 : /do e2Action state S3 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S2 -down[#000000]-> S3 : E2 +[*] --> S1 +S1 -down-> S2 : E1 +S2 -down-> S3 : E2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml index 4c99b381c..f9ea01913 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachine.puml @@ -9,13 +9,13 @@ state S1 { state S12 - [*] -[#000000]-> S11 + [*] --> S11 } state S2 -[*] -[#000000]-> S1 -S11 -down[#000000]-> S12 : E1 -S1 -down[#000000]-> S2 : E2 +[*] --> S1 +S11 -down-> S12 : E1 +S1 -down-> S2 : E2 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.puml index fbb1320eb..a1225b964 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.puml @@ -12,19 +12,19 @@ state S2 { state S31 - [*] -[#000000]-> S30 + [*] --> S30 } - [*] -[#000000]-> S20 + [*] --> S20 } state S3 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S20 -down[#000000]-> S21 : E2 -S30 -down[#000000]-> S31 : E3 -S2 -down[#000000]-> S3 : E4 +[*] --> S1 +S1 -down-> S2 : E1 +S20 -down-> S21 : E2 +S30 -down-> S31 : E3 +S2 -down-> S3 : E4 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml index 3a89945ab..cf9fdf8a4 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-timers.puml @@ -13,10 +13,10 @@ state S5 S5 : /entry s5Entry -[*] -[#000000]-> S1 -S1 -down[#000000]-> S4 : E2 -S1 -down[#000000]-> S2 : E1 -S2 -down[#000000]-> S3 : every 1 second -S4 -down[#000000]-> S5 : after 1 second +[*] --> S1 +S1 -down-> S2 : E1 +S1 -down-> S4 : E2 +S2 -down-> S3 : every 1 second +S4 -down-> S5 : after 1 second @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml index ea2dad72b..9a994a1c9 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-transitiontypes.puml @@ -8,9 +8,9 @@ state S1 state S2 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1 -S2 -down[#000000]-> S1 : E2 -S2 -down[#000000]-> S2 : E3 +[*] --> S1 +S1 -down-> S2 : E1 +S2 -down-> S1 : E2 +S2 -down-> S2 : E3 @enduml \ No newline at end of file diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.puml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.puml index 13e6d9c7a..3dd9e9d10 100644 --- a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.puml +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/transition-effect-spel.puml @@ -8,7 +8,7 @@ state S1 state S2 -[*] -[#000000]-> S1 -S1 -down[#000000]-> S2 : E1\n/ extendedState.variables.put('key','value') +[*] --> S1 +S1 -down-> S2 : E1\n/ extendedState.variables.put('key','value') @enduml \ No newline at end of file From 935fa333d17896b3e1f3e95a4e9c732d44303f3e Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Thu, 11 Jul 2024 14:01:16 +0200 Subject: [PATCH 12/16] chore: Add Apache license header --- .../plantuml/ContextTransition.java | 50 +- .../statemachine/plantuml/PlantUmlWriter.java | 1436 +++++++++-------- .../plantuml/PlantUmlWriterParameters.java | 476 +++--- .../plantuml/StateMachineHelper.java | 187 ++- .../plantuml/helper/NameGetter.java | 249 +-- .../plantuml/helper/RegionComparator.java | 59 +- .../plantuml/helper/StateComparator.java | 24 +- .../plantuml/helper/TransactionHelper.java | 246 +-- .../plantuml/helper/TransitionComparator.java | 36 +- 9 files changed, 1451 insertions(+), 1312 deletions(-) diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java index 455c08f7b..c11a06cd7 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/ContextTransition.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml; import lombok.AllArgsConstructor; @@ -8,22 +24,22 @@ @AllArgsConstructor @Getter public class ContextTransition { - final S source; - final E event; - final S target; + final S source; + final E event; + final S target; - public static ContextTransition of(@Nullable StateContext stateContext) { - if (stateContext != null) { - return new ContextTransition<>( - stateContext.getSource() != null - ? stateContext.getSource().getId() - : null, - stateContext.getEvent(), - stateContext.getTarget() != null - ? stateContext.getTarget().getId() - : null - ); - } - return null; - } + public static ContextTransition of(@Nullable StateContext stateContext) { + if (stateContext != null) { + return new ContextTransition<>( + stateContext.getSource() != null + ? stateContext.getSource().getId() + : null, + stateContext.getEvent(), + stateContext.getTarget() != null + ? stateContext.getTarget().getId() + : null + ); + } + return null; + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java index 52239c60c..47f021c58 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml; import jakarta.validation.constraints.NotNull; @@ -64,720 +80,720 @@ @SuppressWarnings("squid:S107") public class PlantUmlWriter { - private static final Log log = LogFactory.getLog(PlantUmlWriter.class); - - private static final String INDENT_INCREMENT = " "; - - // History states are handled in a special way: - // 1 - During the 1st pass, History states 'IDs' are 'computed' and collected in 'historyStatesToHistoryId' - // 2 - During the 2nd pass (the main one), 'history' transitions are collected in 'historyTransitions' - // 3 - Then, the collected 'historyTransitions' are added at the end of the PlantUml diagram using 'historyStatesToHistoryId' - - private Map, String> historyStatesToHistoryId; - private List> historyTransitions; - - // Comparators. Used to keep order of region, states and transitions stable in generated puml - - private final StateComparator stateComparator = new StateComparator<>(); - - private final Comparator> transitionComparator = new TransitionComparator<>(); - - private final Comparator> regionComparator = new RegionComparator<>(stateComparator); - - // - - public void save( - @NotNull StateMachine stateMachine, - @NotNull File file - ) throws IOException { - save(stateMachine, null, null, file); - } - - /** - * @param stateMachine stateMachine - * @param stateContext stateContext - * @param plantUmlWriterParameters plantUmlWriterParameters - * @param file filename must be "*.puml" or "*.png" - * @throws IOException - */ - public void save( - @NotNull StateMachine stateMachine, - @Nullable StateContext stateContext, - @Nullable PlantUmlWriterParameters plantUmlWriterParameters, - @NotNull File file - ) throws IOException { - if (plantUmlWriterParameters == null) { - plantUmlWriterParameters = new PlantUmlWriterParameters<>(); - } - String plantUmlDiagram = toPlantUml( - stateMachine, - stateContext, - plantUmlWriterParameters - ); - - if (file.getName().endsWith(".puml")) { - Files.write(file.toPath(), plantUmlDiagram.getBytes()); - } else if (file.getName().endsWith(".png")) { - SourceStringReader reader = new SourceStringReader(plantUmlDiagram); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - reader.outputImage(os); - Files.write(file.toPath(), os.toByteArray()); - } else { - throw new IllegalArgumentException("file name must be *.puml or *.png"); - } - } - - public String toPlantUml(StateMachine stateMachine) { - return toPlantUml(stateMachine, null, null); - } - - /** - * Convert a State machine in PlantUML notation
- * limited support! - * - * @param stateMachine stateMachine - * @param stateContext stateContext - * @param plantUmlWriterParameters plantUmlWriterParameters - * @return plantUml representation of the stateMachine - */ - public String toPlantUml( - StateMachine stateMachine, - @Nullable StateContext stateContext, - @Nullable PlantUmlWriterParameters plantUmlWriterParameters - ) { - if (plantUmlWriterParameters == null) { - plantUmlWriterParameters = new PlantUmlWriterParameters<>(); - } - - // 1st pass: Collecting history states - historyStatesToHistoryId = StateMachineHelper.collectHistoryStates(stateMachine); - historyTransitions = new ArrayList<>(); - - StringBuilder sb = new StringBuilder("@startuml\n") - .append(PlantUmlWriterParameters.getStateDiagramSettings(plantUmlWriterParameters)) - .append("\n"); - - - // 2nd pass: processing statemachine AND collecting history transitions in 'historyTransitions - processRegion(stateMachine, stateContext, plantUmlWriterParameters, sb, "", null); - - // finally, adding the collected history transitions - for (Transition transition : historyTransitions) { - processPseudoStatesTransition( - ContextTransition.of(stateContext), - transition, - transition.getSource(), - transition.getTarget(), - this::getHistoryStateId, - plantUmlWriterParameters, - sb, - "" - ); - } - - sb.append(plantUmlWriterParameters.getHiddenTransitions()); - - sb.append("\n@enduml"); - - log.debug("toPlantUml:" + sb); - - return sb.toString(); - } - - // TODO check this... is this a good idea? - private interface HistoryIdGetter { - String getId(State state); - } - - String getHistoryStateId(State state) { - if (historyStatesToHistoryId.containsKey(state)) { - return historyStatesToHistoryId.get(state); - } else { - return state.getId().toString(); - } - } - - private void processRegion( - @NotNull Region region, - @Nullable StateContext stateContext, - @Nullable PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent, - @Nullable Predicate> transitionAllowed // not working for the moment - ) { - if (plantUmlWriterParameters == null) { - plantUmlWriterParameters = new PlantUmlWriterParameters<>(); - } - - // check 'entry' and 'exit' pseudo states: - // these states MUST NOT be added in this region, BUT in the subregion / submachine they are related to! - Map>> allStates = region.getStates() - .stream() - .collect(Collectors.groupingBy(this::isEntryOrExit)); - - List> entryAndExitStates = allStates.get(Boolean.TRUE); - List> otherStates = allStates.get(Boolean.FALSE); - - // states - processStates( - region, - stateContext, - entryAndExitStates, - otherStates, - plantUmlWriterParameters, - sb, - indent - ); - - // transitions - ContextTransition currentContextTransition = ContextTransition.of(stateContext); - processPseudoStatesTransitions(region, currentContextTransition, plantUmlWriterParameters, sb, indent); - processTransitions(region, plantUmlWriterParameters, currentContextTransition, sb, indent, transitionAllowed); - } - - private Boolean isEntryOrExit(State state) { - if (state.getPseudoState() == null) { - return Boolean.FALSE; - } - return PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()) - || PseudoStateKind.EXIT.equals(state.getPseudoState().getKind()); - } - - // TODO create processState() with optional '{' and '}' to allow text on stereotype states? - // warning -> this change the visual representation of the state, using the 'default' representation :/ - private void processStates( - Region region, - StateContext stateContext, - @Nullable Collection> entryAndExitStates, - List> otherStates, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - S currentState = region.getState() != null - ? region.getState().getId() - : null; - - Collection> regionTransitions = region.getTransitions(); - - // associating each entry to its targets, and each exit to its sources (as per transitions) - Map>> allStates = entryAndExitStates == null ? Map.of() - : entryAndExitStates - .stream() - .collect(Collectors.groupingBy(state -> PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()))); - - List> entryStates = allStates.get(Boolean.TRUE); - Map, List> entryToTargetStates = entryStates == null ? Map.of() : - entryStates.stream().collect( - toMap( - entry -> entry, - entry -> getEntryTargets(entry, regionTransitions), - (s, s2) -> { - throw new IllegalStateException("This should not happen!"); - })); - - List> exitStates = allStates.get(Boolean.FALSE); - Map, List> exitToSourceStates = exitStates == null ? Map.of() : - exitStates.stream().collect( - toMap( - exit -> exit, - exit -> getExitSources(exit, regionTransitions), - (s, s2) -> { - throw new IllegalStateException("This should not happen!"); - })); - - // processing states - otherStates.stream() - .sorted(stateComparator) - .toList() - .forEach(state -> processState(state, currentState, entryToTargetStates, exitToSourceStates, stateContext, plantUmlWriterParameters, sb, indent)); - - sb.append("\n"); - } - - private List getEntryTargets(State entry, Collection> regionTransitions) { - return regionTransitions.stream() - .filter(aTransition -> entry.getId().equals(aTransition.getSource().getId())) - .map(aTransition -> aTransition.getTarget().getId()) - .toList(); - } - - private List getExitSources(State exit, Collection> regionTransitions) { - return regionTransitions.stream() - .filter(aTransition -> exit.getId().equals(aTransition.getTarget().getId())) - .map(aTransition -> aTransition.getSource().getId()) - .toList(); - } - - private void processState( - State state, - S currentState, - Map, List> entryToTargetStates, - Map, List> exitToSourceStates, - StateContext stateContext, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - if (state.isSimple()) { - processSimpleState(state, currentState, plantUmlWriterParameters, sb, indent); - } else if (state.isSubmachineState()) { - if (state instanceof AbstractState abstractState) { - processSubmachine(abstractState, currentState, plantUmlWriterParameters, sb, indent, stateContext, entryToTargetStates, exitToSourceStates); - } - } else if (state.isComposite() || state.isOrthogonal()) { - if (state instanceof RegionState regionState) { - processCompositeOrOrthogonalState(regionState, currentState, plantUmlWriterParameters, sb, indent, stateContext, entryToTargetStates, exitToSourceStates); - } - } else { - throw new NotImplementedException("Unexpected state type " + state.getId()); - } - processStateDescription(state, sb, indent); - } - - private void processCompositeOrOrthogonalState( - RegionState regionState, - S currentState, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent, - StateContext stateContext, - Map, List> entryToTargetStates, - Map, List> exitToSourceStates - ) { - sb.append(""" - %s { - """ - .formatted(stateToString(indent, regionState.getId(), currentState, plantUmlWriterParameters)) - .stripIndent()); - - final String regionIndent = indent + INDENT_INCREMENT; - List> regions = regionState.getRegions().stream() - .sorted(regionComparator) - .toList(); - - for (int i = 0, nRegions = regions.size(); i < nRegions; i++) { - Region subRegion = regions.get(i); - processRegion(subRegion, stateContext, plantUmlWriterParameters, sb, regionIndent, new Predicate>() { - @Override - public boolean test(Transition transition) { - // TODO implement this by checking if source or target state belong to another region ??? + private static final Log log = LogFactory.getLog(PlantUmlWriter.class); + + private static final String INDENT_INCREMENT = " "; + + // History states are handled in a special way: + // 1 - During the 1st pass, History states 'IDs' are 'computed' and collected in 'historyStatesToHistoryId' + // 2 - During the 2nd pass (the main one), 'history' transitions are collected in 'historyTransitions' + // 3 - Then, the collected 'historyTransitions' are added at the end of the PlantUml diagram using 'historyStatesToHistoryId' + + private Map, String> historyStatesToHistoryId; + private List> historyTransitions; + + // Comparators. Used to keep order of region, states and transitions stable in generated puml + + private final StateComparator stateComparator = new StateComparator<>(); + + private final Comparator> transitionComparator = new TransitionComparator<>(); + + private final Comparator> regionComparator = new RegionComparator<>(stateComparator); + + // + + public void save( + @NotNull StateMachine stateMachine, + @NotNull File file + ) throws IOException { + save(stateMachine, null, null, file); + } + + /** + * @param stateMachine stateMachine + * @param stateContext stateContext + * @param plantUmlWriterParameters plantUmlWriterParameters + * @param file filename must be "*.puml" or "*.png" + * @throws IOException + */ + public void save( + @NotNull StateMachine stateMachine, + @Nullable StateContext stateContext, + @Nullable PlantUmlWriterParameters plantUmlWriterParameters, + @NotNull File file + ) throws IOException { + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + } + String plantUmlDiagram = toPlantUml( + stateMachine, + stateContext, + plantUmlWriterParameters + ); + + if (file.getName().endsWith(".puml")) { + Files.write(file.toPath(), plantUmlDiagram.getBytes()); + } else if (file.getName().endsWith(".png")) { + SourceStringReader reader = new SourceStringReader(plantUmlDiagram); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + reader.outputImage(os); + Files.write(file.toPath(), os.toByteArray()); + } else { + throw new IllegalArgumentException("file name must be *.puml or *.png"); + } + } + + public String toPlantUml(StateMachine stateMachine) { + return toPlantUml(stateMachine, null, null); + } + + /** + * Convert a State machine in PlantUML notation
+ * limited support! + * + * @param stateMachine stateMachine + * @param stateContext stateContext + * @param plantUmlWriterParameters plantUmlWriterParameters + * @return plantUml representation of the stateMachine + */ + public String toPlantUml( + StateMachine stateMachine, + @Nullable StateContext stateContext, + @Nullable PlantUmlWriterParameters plantUmlWriterParameters + ) { + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + } + + // 1st pass: Collecting history states + historyStatesToHistoryId = StateMachineHelper.collectHistoryStates(stateMachine); + historyTransitions = new ArrayList<>(); + + StringBuilder sb = new StringBuilder("@startuml\n") + .append(PlantUmlWriterParameters.getStateDiagramSettings(plantUmlWriterParameters)) + .append("\n"); + + + // 2nd pass: processing statemachine AND collecting history transitions in 'historyTransitions + processRegion(stateMachine, stateContext, plantUmlWriterParameters, sb, "", null); + + // finally, adding the collected history transitions + for (Transition transition : historyTransitions) { + processPseudoStatesTransition( + ContextTransition.of(stateContext), + transition, + transition.getSource(), + transition.getTarget(), + this::getHistoryStateId, + plantUmlWriterParameters, + sb, + "" + ); + } + + sb.append(plantUmlWriterParameters.getHiddenTransitions()); + + sb.append("\n@enduml"); + + log.debug("toPlantUml:" + sb); + + return sb.toString(); + } + + // TODO check this... is this a good idea? + private interface HistoryIdGetter { + String getId(State state); + } + + String getHistoryStateId(State state) { + if (historyStatesToHistoryId.containsKey(state)) { + return historyStatesToHistoryId.get(state); + } else { + return state.getId().toString(); + } + } + + private void processRegion( + @NotNull Region region, + @Nullable StateContext stateContext, + @Nullable PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent, + @Nullable Predicate> transitionAllowed // not working for the moment + ) { + if (plantUmlWriterParameters == null) { + plantUmlWriterParameters = new PlantUmlWriterParameters<>(); + } + + // check 'entry' and 'exit' pseudo states: + // these states MUST NOT be added in this region, BUT in the subregion / submachine they are related to! + Map>> allStates = region.getStates() + .stream() + .collect(Collectors.groupingBy(this::isEntryOrExit)); + + List> entryAndExitStates = allStates.get(Boolean.TRUE); + List> otherStates = allStates.get(Boolean.FALSE); + + // states + processStates( + region, + stateContext, + entryAndExitStates, + otherStates, + plantUmlWriterParameters, + sb, + indent + ); + + // transitions + ContextTransition currentContextTransition = ContextTransition.of(stateContext); + processPseudoStatesTransitions(region, currentContextTransition, plantUmlWriterParameters, sb, indent); + processTransitions(region, plantUmlWriterParameters, currentContextTransition, sb, indent, transitionAllowed); + } + + private Boolean isEntryOrExit(State state) { + if (state.getPseudoState() == null) { + return Boolean.FALSE; + } + return PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()) + || PseudoStateKind.EXIT.equals(state.getPseudoState().getKind()); + } + + // TODO create processState() with optional '{' and '}' to allow text on stereotype states? + // warning -> this change the visual representation of the state, using the 'default' representation :/ + private void processStates( + Region region, + StateContext stateContext, + @Nullable Collection> entryAndExitStates, + List> otherStates, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + S currentState = region.getState() != null + ? region.getState().getId() + : null; + + Collection> regionTransitions = region.getTransitions(); + + // associating each entry to its targets, and each exit to its sources (as per transitions) + Map>> allStates = entryAndExitStates == null ? Map.of() + : entryAndExitStates + .stream() + .collect(Collectors.groupingBy(state -> PseudoStateKind.ENTRY.equals(state.getPseudoState().getKind()))); + + List> entryStates = allStates.get(Boolean.TRUE); + Map, List> entryToTargetStates = entryStates == null ? Map.of() : + entryStates.stream().collect( + toMap( + entry -> entry, + entry -> getEntryTargets(entry, regionTransitions), + (s, s2) -> { + throw new IllegalStateException("This should not happen!"); + })); + + List> exitStates = allStates.get(Boolean.FALSE); + Map, List> exitToSourceStates = exitStates == null ? Map.of() : + exitStates.stream().collect( + toMap( + exit -> exit, + exit -> getExitSources(exit, regionTransitions), + (s, s2) -> { + throw new IllegalStateException("This should not happen!"); + })); + + // processing states + otherStates.stream() + .sorted(stateComparator) + .toList() + .forEach(state -> processState(state, currentState, entryToTargetStates, exitToSourceStates, stateContext, plantUmlWriterParameters, sb, indent)); + + sb.append("\n"); + } + + private List getEntryTargets(State entry, Collection> regionTransitions) { + return regionTransitions.stream() + .filter(aTransition -> entry.getId().equals(aTransition.getSource().getId())) + .map(aTransition -> aTransition.getTarget().getId()) + .toList(); + } + + private List getExitSources(State exit, Collection> regionTransitions) { + return regionTransitions.stream() + .filter(aTransition -> exit.getId().equals(aTransition.getTarget().getId())) + .map(aTransition -> aTransition.getSource().getId()) + .toList(); + } + + private void processState( + State state, + S currentState, + Map, List> entryToTargetStates, + Map, List> exitToSourceStates, + StateContext stateContext, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + if (state.isSimple()) { + processSimpleState(state, currentState, plantUmlWriterParameters, sb, indent); + } else if (state.isSubmachineState()) { + if (state instanceof AbstractState abstractState) { + processSubmachine(abstractState, currentState, plantUmlWriterParameters, sb, indent, stateContext, entryToTargetStates, exitToSourceStates); + } + } else if (state.isComposite() || state.isOrthogonal()) { + if (state instanceof RegionState regionState) { + processCompositeOrOrthogonalState(regionState, currentState, plantUmlWriterParameters, sb, indent, stateContext, entryToTargetStates, exitToSourceStates); + } + } else { + throw new NotImplementedException("Unexpected state type " + state.getId()); + } + processStateDescription(state, sb, indent); + } + + private void processCompositeOrOrthogonalState( + RegionState regionState, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent, + StateContext stateContext, + Map, List> entryToTargetStates, + Map, List> exitToSourceStates + ) { + sb.append(""" + %s { + """ + .formatted(stateToString(indent, regionState.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + + final String regionIndent = indent + INDENT_INCREMENT; + List> regions = regionState.getRegions().stream() + .sorted(regionComparator) + .toList(); + + for (int i = 0, nRegions = regions.size(); i < nRegions; i++) { + Region subRegion = regions.get(i); + processRegion(subRegion, stateContext, plantUmlWriterParameters, sb, regionIndent, new Predicate>() { + @Override + public boolean test(Transition transition) { + // TODO implement this by checking if source or target state belong to another region ??? // log.warn("Transition {} from / to another region is not allowed"); - return true; - } - }); - processEntries(currentState, subRegion.getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); - processExits(currentState, subRegion.getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); - // using "--" caused problem ... how to solve this..? + return true; + } + }); + processEntries(currentState, subRegion.getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); + processExits(currentState, subRegion.getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); + // using "--" caused problem ... how to solve this..? /* - if (i != nRegions - 1) { - // Separating regions. see "États concurrents [--, ||]" https://plantuml.com/fr/state-diagram#73b918d90b24a6c6 - sb.append(regionIndent).append("--\n"); - } + if (i != nRegions - 1) { + // Separating regions. see "États concurrents [--, ||]" https://plantuml.com/fr/state-diagram#73b918d90b24a6c6 + sb.append(regionIndent).append("--\n"); + } */ - } - sb.append(""" - %s} - """.formatted(indent)); - } - - private void processSubmachine( - AbstractState abstractState, - S currentState, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent, - StateContext stateContext, - Map, List> entryToTargetStates, - Map, List> exitToSourceStates - ) { - sb.append(""" - %s { - """ - .formatted(stateToString(indent, abstractState.getId(), currentState, plantUmlWriterParameters)) - .stripIndent()); - final String regionIndent = indent + INDENT_INCREMENT; - processRegion(abstractState.getSubmachine(), stateContext, plantUmlWriterParameters, sb, regionIndent, null); - processEntries(currentState, abstractState.getSubmachine().getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); - processExits(currentState, abstractState.getSubmachine().getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); - sb.append(""" - %s} - """.formatted(indent)); - } - - private void processSimpleState( - State state, - S currentState, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - if (state.getPseudoState() != null) { - processPseudoState(state, currentState, plantUmlWriterParameters, sb, indent); - } else { + } + sb.append(""" + %s} + """.formatted(indent)); + } + + private void processSubmachine( + AbstractState abstractState, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent, + StateContext stateContext, + Map, List> entryToTargetStates, + Map, List> exitToSourceStates + ) { + sb.append(""" + %s { + """ + .formatted(stateToString(indent, abstractState.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + final String regionIndent = indent + INDENT_INCREMENT; + processRegion(abstractState.getSubmachine(), stateContext, plantUmlWriterParameters, sb, regionIndent, null); + processEntries(currentState, abstractState.getSubmachine().getStates(), entryToTargetStates, plantUmlWriterParameters, sb, regionIndent); + processExits(currentState, abstractState.getSubmachine().getStates(), exitToSourceStates, plantUmlWriterParameters, sb, regionIndent); + sb.append(""" + %s} + """.formatted(indent)); + } + + private void processSimpleState( + State state, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + if (state.getPseudoState() != null) { + processPseudoState(state, currentState, plantUmlWriterParameters, sb, indent); + } else { // TODO allow plantUmlWriterParameters to add links in description ?? // eg: state "CarWithWheel [[http://plantuml.com/state-diagram]]" as CarWithWheel - sb.append(""" - %s - """ - .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) - .stripIndent()); - } - } - - private void processEntries( - S currentState, - Collection> regionStates, - Map, List> entryToTargetStates, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - entryToTargetStates.keySet() - .forEach(entryState -> entryToTargetStates.get(entryState) - .stream() - .filter(targetStateId -> regionStates.stream().anyMatch(regionState -> targetStateId.equals(regionState.getId()))) - .forEach(s -> processPseudoState(entryState, currentState, plantUmlWriterParameters, sb, indent))); - } - - private void processExits( - S currentState, - Collection> regionStates, - Map, List> exitToSourceStates, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - exitToSourceStates.keySet() - .forEach(exitState -> exitToSourceStates.get(exitState) - .stream() - .filter(sourceStateId -> regionStates.stream().anyMatch(regionState -> sourceStateId.equals(regionState.getId()))) - .forEach(s -> processPseudoState(exitState, currentState, plantUmlWriterParameters, sb, indent))); - } - - private void processPseudoState( - State state, - S currentState, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - PseudoStateKind pseudoStateKind = state.getPseudoState().getKind(); - switch (pseudoStateKind) { - case INITIAL -> { - if (StateMachineUtils.isPseudoState(state, PseudoStateKind.INITIAL)) { - sb.append(""" - %s - """ - .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) - .stripIndent()); - } - } - case HISTORY_SHALLOW, HISTORY_DEEP -> { - // no-op - // History states are NOT added in the diagram as per themselves - // They are added 'just' by creating a transition involving an history state, i.e., [H] or [H*] - // see historyStatesToHistoryId and historyTransitions - } - case ENTRY, EXIT -> sb.append(""" - %sstate %s <<%s>> - """ - .formatted( - indent, - state.getId(), - getPseudoStatePlantUmlStereotype(pseudoStateKind) - ).stripIndent()); - case END, CHOICE, FORK, JOIN, JUNCTION -> sb.append(""" - %s'%s <<%s>> - %sstate %s <<%s>> - %snote left of %s %s: %s - """ - .formatted( - indent, - state.getId(), - pseudoStateKind.name(), - indent, - state.getId(), - getPseudoStatePlantUmlStereotype(pseudoStateKind), - indent, - state.getId(), - plantUmlWriterParameters.getStateColor(state.getId(), currentState), - state.getId() - ).stripIndent()); - } - } - - /** - * Return a PlantUML stereotype for a given PseudoStateKind
- * we use <> stereotype for JUNCTION
- * see
UML 2 - State Machine Diagram - * - * @param pseudoStateKind pseudoStateKind - * @return PlantUml stereotype corresponding to pseudoStateKind - */ - private String getPseudoStatePlantUmlStereotype(PseudoStateKind pseudoStateKind) { - return switch (pseudoStateKind) { - case INITIAL, JUNCTION -> "start"; - case ENTRY, EXIT -> pseudoStateKind.name().toLowerCase() + "Point"; - default -> pseudoStateKind.name().toLowerCase(); - }; - } - - private String stateToString( - String indent, S state, - S currentState, - PlantUmlWriterParameters plantUmlWriterParameters - ) { - return "%sstate %s %s" - .formatted( - indent, - state, - plantUmlWriterParameters.getStateColor(state, currentState) - ); - } - - private void processStateDescription( - State state, - StringBuilder sb, - String indent - ) { - for (E deferredEvent : state.getDeferredEvents()) { - sb.append(""" - %s%s : %s /defer - """ - .formatted( - indent, - state.getId(), - deferredEvent - ) - .stripIndent() - ); - } - for (Function, Mono> entryAction : state.getEntryActions()) { - sb.append(""" - %s%s : /entry %s - """ - .formatted( - indent, - state.getId(), - NameGetter.getName(entryAction) - ) - .stripIndent() - ); - } - for (Function, Mono> stateAction : state.getStateActions()) { - sb.append(""" - %s%s : /do %s - """ - .formatted( - indent, - state.getId(), - NameGetter.getName(stateAction) - ) - .stripIndent() - ); - } - for (Function, Mono> exitAction : state.getExitActions()) { - sb.append(""" - %s%s : /exit %s - """ - .formatted( - indent, - state.getId(), - NameGetter.getName(exitAction) - ) - .stripIndent() - ); - } - } - - - private void processPseudoStatesTransitions( - Region region, - @Nullable ContextTransition currentContextTransition, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - List> states = region.getStates().stream() - .sorted(stateComparator) - .toList(); - for (State state : states) { - // collecting transitions for 'pseudoStateKinds' - if (state.getPseudoState() != null) { - PseudoStateKind pseudoStateKind = state.getPseudoState().getKind(); - switch (pseudoStateKind) { - // already taken care of ;-) - case INITIAL, END, CHOICE, JOIN, JUNCTION, ENTRY, EXIT, HISTORY_SHALLOW, HISTORY_DEEP -> { - // already taken care of ;-) - } - case FORK -> { - // TODO why do I have to do this here? try to do it elsewhere? - for (State nextState : ((ForkPseudoState) state.getPseudoState()).getForks()) { - processPseudoStatesTransition( - currentContextTransition, - null, - state, - nextState, - null, - plantUmlWriterParameters, - sb, - indent - ); - } - } - } - } - } - sb.append("\n"); - } - - private void processPseudoStatesTransition( - @Nullable ContextTransition currentContextTransition, - @Nullable Transition transition, - State sourceState, - State targetState, - @Nullable HistoryIdGetter historyIdGetter, - PlantUmlWriterParameters plantUmlWriterParameters, - StringBuilder sb, - String indent - ) { - S source = sourceState.getId(); - S target = targetState.getId(); - sb.append(""" - %s%s - %s%s -%s%s-> %s %s - """ - .formatted( - indent, - historyIdGetter == null ? "" : "'" + source + " -> " + target, // if history transition, add a comment with 'real' state names - indent, - historyIdGetter == null ? sourceState.getId() : historyIdGetter.getId(sourceState), - plantUmlWriterParameters.getDirection(source, target), - plantUmlWriterParameters.getArrowColor( - currentContextTransition != null - && ( - currentContextTransition.getSource() == source - // && currentContextTransition.event == transition.getTrigger().getEvent() - && currentContextTransition.getTarget() == target - ) - ), - historyIdGetter == null ? targetState.getId() : historyIdGetter.getId(targetState), - TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) - ) - .stripIndent() - ); - } - - private void processTransitions( - Region region, - PlantUmlWriterParameters plantUmlWriterParameters, - @Nullable ContextTransition currentContextTransition, - StringBuilder sb, - String indent, - @Nullable Predicate> transitionAllowed - ) { - - // initial transition - Transition initialTransition = TransactionHelper.getInitialTransition(region); - if (initialTransition != null) { - addTransition( - sb, - indent, - "[*]", // transition from initial state - null, - initialTransition.getTarget().getId(), - plantUmlWriterParameters, - plantUmlWriterParameters.getArrowColor(false), - initialTransition - ); - } - - // region's transitions - for (Transition transition : - region.getTransitions().stream().sorted(transitionComparator).toList() - ) { - // in orthogonalRegion, we do NOT allow transition to states from another region! - if (transitionAllowed != null && !transitionAllowed.test(transition)) { - return; - } - - // collect history transitions: they will be added to the diagram later on - if (isHistoryTransition(transition)) { - historyTransitions.add(transition); - } else { - S source = transition.getSource().getId(); - S target = transition.getTarget().getId(); - - switch (transition.getKind()) { - case EXTERNAL, INTERNAL, LOCAL -> { - String arrowColor = plantUmlWriterParameters.getArrowColor( - currentContextTransition != null - && transition.getTrigger() != null - && ( - currentContextTransition.getSource() == source - && currentContextTransition.getEvent() == transition.getTrigger().getEvent() - && currentContextTransition.getTarget() == target - ) - ); - addTransition(sb, indent, source.toString(), source, target, plantUmlWriterParameters, arrowColor, transition); - } - case INITIAL -> throw new NotImplementedException( - "Unexpected INITIAL transition! They are handled in processInitialTransition(), not here! Check why we reached this exception!" - ); - } - } - } - } - - private void addTransition( - StringBuilder sb, - String indent, - String sourceLabel, - S source, - S target, - PlantUmlWriterParameters plantUmlWriterParameters, - String arrowColor, - Transition transition - ) { - sb.append(""" - %s%s -%s%s-> %s %s - """ - .formatted( - indent, - sourceLabel, - source == null ? "" : plantUmlWriterParameters.getDirection(source, target), - arrowColor, - target, - TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) - ) - .stripIndent() - ); - if (StringUtils.isNotBlank(transition.getName())) { - sb.append(""" - %snote on link - %s %s - %send note - """ - .formatted( - indent, - indent, - transition.getName(), - indent - )); - } - } - - private boolean isHistoryTransition(@NotNull Transition transition) { - return isHistoryState(transition.getSource()) || isHistoryState(transition.getTarget()); - } - - private boolean isHistoryState(@NotNull State state) { - if (state.getPseudoState() == null) { - return Boolean.FALSE; - } - return PseudoStateKind.HISTORY_SHALLOW.equals(state.getPseudoState().getKind()) - || PseudoStateKind.HISTORY_DEEP.equals(state.getPseudoState().getKind()); - } + sb.append(""" + %s + """ + .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + } + } + + private void processEntries( + S currentState, + Collection> regionStates, + Map, List> entryToTargetStates, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + entryToTargetStates.keySet() + .forEach(entryState -> entryToTargetStates.get(entryState) + .stream() + .filter(targetStateId -> regionStates.stream().anyMatch(regionState -> targetStateId.equals(regionState.getId()))) + .forEach(s -> processPseudoState(entryState, currentState, plantUmlWriterParameters, sb, indent))); + } + + private void processExits( + S currentState, + Collection> regionStates, + Map, List> exitToSourceStates, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + exitToSourceStates.keySet() + .forEach(exitState -> exitToSourceStates.get(exitState) + .stream() + .filter(sourceStateId -> regionStates.stream().anyMatch(regionState -> sourceStateId.equals(regionState.getId()))) + .forEach(s -> processPseudoState(exitState, currentState, plantUmlWriterParameters, sb, indent))); + } + + private void processPseudoState( + State state, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + PseudoStateKind pseudoStateKind = state.getPseudoState().getKind(); + switch (pseudoStateKind) { + case INITIAL -> { + if (StateMachineUtils.isPseudoState(state, PseudoStateKind.INITIAL)) { + sb.append(""" + %s + """ + .formatted(stateToString(indent, state.getId(), currentState, plantUmlWriterParameters)) + .stripIndent()); + } + } + case HISTORY_SHALLOW, HISTORY_DEEP -> { + // no-op + // History states are NOT added in the diagram as per themselves + // They are added 'just' by creating a transition involving an history state, i.e., [H] or [H*] + // see historyStatesToHistoryId and historyTransitions + } + case ENTRY, EXIT -> sb.append(""" + %sstate %s <<%s>> + """ + .formatted( + indent, + state.getId(), + getPseudoStatePlantUmlStereotype(pseudoStateKind) + ).stripIndent()); + case END, CHOICE, FORK, JOIN, JUNCTION -> sb.append(""" + %s'%s <<%s>> + %sstate %s <<%s>> + %snote left of %s %s: %s + """ + .formatted( + indent, + state.getId(), + pseudoStateKind.name(), + indent, + state.getId(), + getPseudoStatePlantUmlStereotype(pseudoStateKind), + indent, + state.getId(), + plantUmlWriterParameters.getStateColor(state.getId(), currentState), + state.getId() + ).stripIndent()); + } + } + + /** + * Return a PlantUML stereotype for a given PseudoStateKind
+ * we use <> stereotype for JUNCTION
+ * see UML 2 - State Machine Diagram + * + * @param pseudoStateKind pseudoStateKind + * @return PlantUml stereotype corresponding to pseudoStateKind + */ + private String getPseudoStatePlantUmlStereotype(PseudoStateKind pseudoStateKind) { + return switch (pseudoStateKind) { + case INITIAL, JUNCTION -> "start"; + case ENTRY, EXIT -> pseudoStateKind.name().toLowerCase() + "Point"; + default -> pseudoStateKind.name().toLowerCase(); + }; + } + + private String stateToString( + String indent, S state, + S currentState, + PlantUmlWriterParameters plantUmlWriterParameters + ) { + return "%sstate %s %s" + .formatted( + indent, + state, + plantUmlWriterParameters.getStateColor(state, currentState) + ); + } + + private void processStateDescription( + State state, + StringBuilder sb, + String indent + ) { + for (E deferredEvent : state.getDeferredEvents()) { + sb.append(""" + %s%s : %s /defer + """ + .formatted( + indent, + state.getId(), + deferredEvent + ) + .stripIndent() + ); + } + for (Function, Mono> entryAction : state.getEntryActions()) { + sb.append(""" + %s%s : /entry %s + """ + .formatted( + indent, + state.getId(), + NameGetter.getName(entryAction) + ) + .stripIndent() + ); + } + for (Function, Mono> stateAction : state.getStateActions()) { + sb.append(""" + %s%s : /do %s + """ + .formatted( + indent, + state.getId(), + NameGetter.getName(stateAction) + ) + .stripIndent() + ); + } + for (Function, Mono> exitAction : state.getExitActions()) { + sb.append(""" + %s%s : /exit %s + """ + .formatted( + indent, + state.getId(), + NameGetter.getName(exitAction) + ) + .stripIndent() + ); + } + } + + + private void processPseudoStatesTransitions( + Region region, + @Nullable ContextTransition currentContextTransition, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + List> states = region.getStates().stream() + .sorted(stateComparator) + .toList(); + for (State state : states) { + // collecting transitions for 'pseudoStateKinds' + if (state.getPseudoState() != null) { + PseudoStateKind pseudoStateKind = state.getPseudoState().getKind(); + switch (pseudoStateKind) { + // already taken care of ;-) + case INITIAL, END, CHOICE, JOIN, JUNCTION, ENTRY, EXIT, HISTORY_SHALLOW, HISTORY_DEEP -> { + // already taken care of ;-) + } + case FORK -> { + // TODO why do I have to do this here? try to do it elsewhere? + for (State nextState : ((ForkPseudoState) state.getPseudoState()).getForks()) { + processPseudoStatesTransition( + currentContextTransition, + null, + state, + nextState, + null, + plantUmlWriterParameters, + sb, + indent + ); + } + } + } + } + } + sb.append("\n"); + } + + private void processPseudoStatesTransition( + @Nullable ContextTransition currentContextTransition, + @Nullable Transition transition, + State sourceState, + State targetState, + @Nullable HistoryIdGetter historyIdGetter, + PlantUmlWriterParameters plantUmlWriterParameters, + StringBuilder sb, + String indent + ) { + S source = sourceState.getId(); + S target = targetState.getId(); + sb.append(""" + %s%s + %s%s -%s%s-> %s %s + """ + .formatted( + indent, + historyIdGetter == null ? "" : "'" + source + " -> " + target, // if history transition, add a comment with 'real' state names + indent, + historyIdGetter == null ? sourceState.getId() : historyIdGetter.getId(sourceState), + plantUmlWriterParameters.getDirection(source, target), + plantUmlWriterParameters.getArrowColor( + currentContextTransition != null + && ( + currentContextTransition.getSource() == source + // && currentContextTransition.event == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target + ) + ), + historyIdGetter == null ? targetState.getId() : historyIdGetter.getId(targetState), + TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) + ) + .stripIndent() + ); + } + + private void processTransitions( + Region region, + PlantUmlWriterParameters plantUmlWriterParameters, + @Nullable ContextTransition currentContextTransition, + StringBuilder sb, + String indent, + @Nullable Predicate> transitionAllowed + ) { + + // initial transition + Transition initialTransition = TransactionHelper.getInitialTransition(region); + if (initialTransition != null) { + addTransition( + sb, + indent, + "[*]", // transition from initial state + null, + initialTransition.getTarget().getId(), + plantUmlWriterParameters, + plantUmlWriterParameters.getArrowColor(false), + initialTransition + ); + } + + // region's transitions + for (Transition transition : + region.getTransitions().stream().sorted(transitionComparator).toList() + ) { + // in orthogonalRegion, we do NOT allow transition to states from another region! + if (transitionAllowed != null && !transitionAllowed.test(transition)) { + return; + } + + // collect history transitions: they will be added to the diagram later on + if (isHistoryTransition(transition)) { + historyTransitions.add(transition); + } else { + S source = transition.getSource().getId(); + S target = transition.getTarget().getId(); + + switch (transition.getKind()) { + case EXTERNAL, INTERNAL, LOCAL -> { + String arrowColor = plantUmlWriterParameters.getArrowColor( + currentContextTransition != null + && transition.getTrigger() != null + && ( + currentContextTransition.getSource() == source + && currentContextTransition.getEvent() == transition.getTrigger().getEvent() + && currentContextTransition.getTarget() == target + ) + ); + addTransition(sb, indent, source.toString(), source, target, plantUmlWriterParameters, arrowColor, transition); + } + case INITIAL -> throw new NotImplementedException( + "Unexpected INITIAL transition! They are handled in processInitialTransition(), not here! Check why we reached this exception!" + ); + } + } + } + } + + private void addTransition( + StringBuilder sb, + String indent, + String sourceLabel, + S source, + S target, + PlantUmlWriterParameters plantUmlWriterParameters, + String arrowColor, + Transition transition + ) { + sb.append(""" + %s%s -%s%s-> %s %s + """ + .formatted( + indent, + sourceLabel, + source == null ? "" : plantUmlWriterParameters.getDirection(source, target), + arrowColor, + target, + TransactionHelper.getTransitionDescription(transition, plantUmlWriterParameters) + ) + .stripIndent() + ); + if (StringUtils.isNotBlank(transition.getName())) { + sb.append(""" + %snote on link + %s %s + %send note + """ + .formatted( + indent, + indent, + transition.getName(), + indent + )); + } + } + + private boolean isHistoryTransition(@NotNull Transition transition) { + return isHistoryState(transition.getSource()) || isHistoryState(transition.getTarget()); + } + + private boolean isHistoryState(@NotNull State state) { + if (state.getPseudoState() == null) { + return Boolean.FALSE; + } + return PseudoStateKind.HISTORY_SHALLOW.equals(state.getPseudoState().getKind()) + || PseudoStateKind.HISTORY_DEEP.equals(state.getPseudoState().getKind()); + } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java index 35080327f..ab0877c18 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/plantuml/PlantUmlWriterParameters.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.plantuml; import lombok.*; @@ -16,235 +32,235 @@ */ public class PlantUmlWriterParameters { - private static final Log log = LogFactory.getLog(PlantUmlWriterParameters.class); - - public static final String DEFAULT_STATE_DIAGRAM_SETTINGS = """ - 'https://plantuml.com/state-diagram - - 'hide description area for state without description - hide empty description - - 'https://plantuml.com/fr/skinparam - 'https://plantuml-documentation.readthedocs.io/en/latest/formatting/all-skin-params.html - 'https://plantuml.com/fr/color - skinparam BackgroundColor white - skinparam DefaultFontColor black - 'skinparam DefaultFontName Impact - skinparam DefaultFontSize 14 - skinparam DefaultFontStyle Normal - skinparam NoteBackgroundColor #FEFFDD - skinparam NoteBorderColor black - - skinparam state { - ArrowColor black - BackgroundColor #F1F1F1 - BorderColor #181818 - FontColor black - ' FontName Impact - FontSize 14 - FontStyle Normal - } - """; - - @Setter - private String stateDiagramSettings = DEFAULT_STATE_DIAGRAM_SETTINGS; - - public static String getStateDiagramSettings(@Nullable PlantUmlWriterParameters plantUmlWriterParameters) { - return plantUmlWriterParameters == null - ? DEFAULT_STATE_DIAGRAM_SETTINGS - : plantUmlWriterParameters.getStateDiagramSettings(); - } - - private String getStateDiagramSettings() { - return stateDiagramSettings == null - ? "" - : stateDiagramSettings; - } - - /** - * Direction of an arrow connecting 2 States - */ - public enum Direction { - UP, - DOWN, - LEFT, - RIGHT - } - - - @Value - @EqualsAndHashCode - private static class Connection { - S source; - S target; - } - - /** - * Map of ( (sourceSate, targetState) -> Direction ) - */ - private final Map, Direction> arrows = new HashMap<>(); - - public PlantUmlWriterParameters arrowDirection(S source, Direction direction, S target) { - arrows.put(new Connection<>(source, target), direction); - return this; - } - - /** - * At least one of source and target must be non-null
- * See implementation for 'arrow direction rule priority' - * - * @param source source State - * @param target target State - * @return Direction.name() - */ - String getDirection(S source, S target) { - if (source == null && target == null) { - throw new IllegalArgumentException("source and target state cannot both be null!"); - } - Connection sourceAndTarget = new Connection<>(source, target); - if (arrows.containsKey(sourceAndTarget)) { - return arrows.get(sourceAndTarget).name().toLowerCase(); - } - Connection sourceOnly = new Connection<>(source, null); - Connection targetOnly = new Connection<>(null, target); - if (arrows.containsKey(sourceOnly) - && arrows.containsKey(targetOnly) - && !arrows.get(sourceOnly).equals(arrows.get(targetOnly)) - ) { - log.warn( - String.format("Two 'unary' 'arrowDirection' rules found for (%s, %s) with DIFFERENT values! Using 'target' rule!", source, target)); - return arrows.get(targetOnly).name().toLowerCase(); - } else if (arrows.containsKey(sourceOnly)) { - return arrows.get(sourceOnly).name().toLowerCase(); - } else if (arrows.containsKey(targetOnly)) { - return arrows.get(targetOnly).name().toLowerCase(); - } else { - return Direction.DOWN.name().toLowerCase(); - } - } - - /** - * Map of ( Connection(sourceSate, targetState) -> Direction ) - * Used to add EXTRA HIDDEN arrows, which are just helping with diagram layout.
- * This is typically useful to 'force' the position of a state comparing to another, EVEN IF THESE TWO STATES AR NOT CONNECTED in the statemachine :-)
- *