About a week ago someone posted on the Fusebox mailing list looking for a way to generate flowcharts from his Fusebox XML files. Adalon was suggested, but it didn't do quite what he was looking for. So I sunk some of my spare time into building such a tool, and even managed to refrain from using Groovy and kept it all CFML. All the layout stuff is custom, and the drawing is all via the CF8 image functions. The imageTranslateDrawingAxis function, in particular, was of great utility, allowing me to do all my drawing in local space, rather than absolute space. This means that every box starts at (0, 0), regardless of where it eventually ends up on the chart, so the drawing of each element is totally encapsulated on it's own coordinate space.
It's easiest to show with an example, so consider this contrived circuit.xml:
<circuit> <fuseaction name="home"> <xfa name="page" value="page" /> <if condition="structKeyExists(attributes, 'name')"> <true> <loop collection="attributes" item="i"> <set name="m" value="#i#" /> </loop> <set name="message" value="Hello, strange unknown person!" /> </true> <false> <set name="message" value="Hello, #name#!" /> <relocate /> <xfa name="exit" /> </false> </if> <include template="dsp_home" contentvariable="bodyContent" /> <do action="lay_default" /> </fuseaction> <fuseaction name="page"> <include template="dsp_page" contentvariable="bodyContent" /> <do action="lay_default" /> </fuseaction> <fuseaction name="lay_default"> <if condition="structKeyExists(attributes, 'showNav')"> <true> <do action="nav" contentvariable="nav" /> </true> <false> <loop> <set name="message" value="Hello, strange unknown person!" /> <do action="nav" contentvariable="nav" /> </loop> </false> </if> <include template="lay_default" /> </fuseaction> <fuseaction name="nav"> <xfa name="home" value="home" /> <xfa name="page" value="page" /> <include template="dsp_nav" /> </fuseaction> </circuit>
Yes, I understand that it wouldn't actually execute in real life (there are a number of missing attributes), but that's not the point. The generated flowcharts for the 'home' and 'nav' fuseactions are floating to the right. You can click the 'home' one to view it full size.
The UI is pretty simple, you enter the path to either a single circuit.xml file (as above), or your fusebox.xml file and hit 'Go'. Then you can pick from the list of available fuseactions to act as your entry point, choose whether you'd like to include global pre/postprocess actions and pre/postfuseaction on the flowchart, and it'll draw it all out for you as a nice little PNG.
If you turn on the implicit fuseactions the charts can get pretty large pretty fast, but even generating some pretty gnarly charts for a sizeable app we have at Mentor didn't take more than a second or two.
If you'd just like to render a single circuit's stuff, you can point it directly at the circuit file, and you'll get an error box for any <do> verbs that point outside the circuit, so you'll still get a full chart, with the external dependencies called out explicitly.
I've made the code available in a zip archive if you'd like to run it yourself (or just poke around). Just unzip it somewhere, and hit index.cfm in the browser. The example circuit.xml above is included, so you can regenerate the demo charts I've shown here, or point it at your own Fusebox app and get a look at what you're doing.
The code itself is hardly an example of eloquent design, since this was just a little "screwing around" project. The drawing stuff is pretty devoid of magic numbers, but I made no attempt to fully/properly decompose my types or to really enforce DRY throughout. It is what it is: something to keep me from getting bored that I though might be of interest to others.
barney, you've done it again. this is incredibly cool!
but having not loooked at fusebox xml before, I am, um, aghast. it really looks like that? like…, seriously, that is how controllers are done in fusebox? wow.
One other question: you refrained from doing it in groovy, but I'm wondering: if you were to do it in groovy, would it have been as easy as doing it with CF's image functions?
It depends on what you mean by "controller". Like in Model-Glue or Mach-II, the circuit.xml file in Fusebox represents a "flow" descriptor. You can embed logic in the XML, of course, but generally it's packaged into either fuses or CFC methods. You end up with XML that looks rather like MG or M-II XML, except that it's explicit invocation instead of implicit invocation. The typical verbs are INCLUDE (include a CFM, usually a view), SET (set a variable or invoke a CFC method), XFA (define an self-referential exit link), and DO (invoke another fuseaction). So your controllers usually end up as either CFMs or CFCs, your views as CFMs, and the XML is used to acquire the data you need to render the view and little else. To sum all that up, if you have a "real" Fusebox app with circuit.xml that looks like that example, you're probably doing it wrong. ;)
And it's worth mentioning that XML is totally optional; you can do everything with convention and/or CFCs anymore.
Regarding Groovy, it's been a while since I've done much raw painting with Java, but from what I recall, it's pretty much identical to what CF provides. The CF functions are relatively thin wrappers, though they smoothed the input/output stuff. So I don't know that it'd be much different, though I do know with Groovy I'd be able to go from my BufferedImage to the output stream without having to use the filesystem like I have to do on CF.
imageDrawLine(img, 0, 0, 10, 10);
versus
g.drawLine(0, 0, 10, 10);
Pretty much a wash, though I like the OO style better.
That is pretty darn sharp.
[...] Contact « Fusebox XML Flowchart Generator [...]
Barney,
Very cool tool, especially when you start working with an app you are not familiar with. Whats your thoughts on possibly displaying the condition for the ?
Dan
Dan,
On line 26 of fork.cfc the variable 'txt' is set to the text that should be displayed in the rhombus. Currently the value is #my.verb.type#, which will always be "if". If you were to change that to #my.verb.attributes.condition# instead, you'd get the condition displayed instead. Note that that's not going to resize the rhombus based on the length of the condition, it'll just draw different text, quite possibly overflowing. If you want to change the rhombus's size, you'll need to dig into the guts of the draw() and measureInternal() methods to not only resize the rhombus, but also make all the arrows, spacing, and positions of the nested verbs correspond to the new rhombus size.
Loops would benefit from similar display, though it's significantly more complicated, because you need to label the break/continue arrows, and the way they are controlled is varied amongst the different types of loops.
I see what you mean. Yeah, that sounds pretty hairy to make all of those changes to try and accommodate the condition. Actually, if there was a way to bump it up 1 space, the text outputted would be above the horizontal lines coming off of the decision symbol.
removed the " + getTextHeight()" from line 50 of fork.cfc and it should be above the lines.
This is amazing. I removed the " + getTextHeight()" but in place of that I added " -15″ which lifts it up even higher and makes it more readable.
Thanks much!
Thank you very much!
Really good work and I really appreciate it! Helped me a lot with my new projects in order to understand the flow
I have one issue though..
If i give the fusebox.xml and I check Pre/Post Fuseactions i get the following error
Element VERBS is undefined in VERB.
The error occurred in /flowgen/inline.cfc: line 13
Panos,
I'm not sure what the issue is, but you can avoid the error by replacing 'verb.verbs' with
iif(structKeyExists(verb, "verbs"), "verb.verbs", arrayNew(1))
which will just synthesize an empty verb list. It's entirely possible that I've missed some edge case with the XML format and that's the root issue; this was just an ad hoc project I threw together so I didn't do any sort of edge analysis. If you wrap line 13 with a cftry..cfcatch and just dump the 'verb' struct if an error is raised, that'll probably give you some insight as to what the problem might be.