Wednesday, 6 April 2011

Issue Library - how it started

A To Do list grows up. 

Some background on one of our projects: our in-house Issue Library, a Seaside application to track workflow. 

The Issue Library started off as a simple online 'to do' list, a way to track my own workload and to experiment with Seaside code. Others started to use it, and eventually it became a real application. A nice bit of guerilla marketing; a home grown issue tracking project would never have been approved. We did look at some open source options, but in order to make an issue tracker useful to a wide audience, it seems that you need to have a lot of options, with the app eventually getting really big and complicated.  Gjaller looked promising: I really liked the idea of working with a configurable Seaside code base. But I had no luck getting it to work.  

The design goals were to make it web based, simple, easy to update and more than a toy.  It needed to be real enough to learn the solid Seaside development details. Data is stored in XML files, one per issue.  Files are read at image startup and written when changed.  Since the Seaside image is the only thing that updates the XML files, the data in memory and on disk is consistent (and trivial to check).  Object are always in memory which give us excellent performance. Writes are fast, about 10ms. I've used this technique with my home 'Quicken' type app.  As long as the image size is reasonable, it works great. Right now the image, which also runs our Patch Manager, is about 225MB. As it grows, old issues will be archived.

Objects which map to XML files are subclassed from our an abstract class with all the XML support code. Read / write is done with #loadFile + #parseXML and #saveFile + #buildXML. 

Parsing is done with VW's XMLParser

[xml := [XML.XMLParser new
validate: false;
parseElements: stream]
on: XML.BadCharacterSignal
do: [:ex | ex resume]] 

Values are then extracted by interating over the XML elements.  Each element is sent as an argument to #parseElement: which subclasses implement.  As each element is found, the corresponding value is set and the next element processed. 

A utility method is used to find the tags. 

parseElement: anElement tag: aString doText: aBlock
anElement tag isNil ifTrue: [^false].
anElement tag type = aString ifFalse: [^false].
anElement elements isEmpty ifTrue: [^true].
aBlock value: anElement elements first text.

For example, extracting the 'title' element is done with...
(self parseElement: anElement tag: 'title' doText: [:value | self title: value]) ifTrue: [^self].
...title is initilized to a new String, so if none in found in the XML file, or if it's empty, the title will stay as initialized.  Other string attributes are set to '<none>' so show that they were never set.

Collection attributes are extracted by iterating over the nested collection, using another utility method...

parseElement: anElement tag: aString doEach: aBlock
anElement tag isNil ifTrue: [^false].
anElement tag type = aString ifFalse: [^false].
anElement elements isEmpty ifTrue: [^true].
anElement elements do: [:each | aBlock value: each].

To extract the list of comments, we use...

(self parseElement: anElement tag: 'comments' doEach: [:value | self parseCommentElement: value]) ifTrue: [^self].

...we create a new instance of a comment object and pass on the XML elements for it to parse...

parseCommentElement: anElement
| comment | 
((anElement tag type) = 'issuecomment') ifFalse: [^self error: 'XML parse error... expected ''issuecomment'' tag'].
comment := TSissueComment newForElement: anElement issueFile: self.
self comments add: comment.

Building the XML is done by adding XML nodes to an XML document.  This could probably be made cleaner with some utility methods, but it's a very static method.
| document node | 
document := XML.Document new.
document addNode: (XML.PI name: 'xml' text: 'version="1.0"').
document addNode: (node := XML.Element tag: 'issue').
node addNode: (XML.Element tag: 'id' elements: (Array with: (XML.Text text: self id printString))).
node addNode: (XML.Element tag: 'title' elements: (Array with: (XML.Text text: self title asOneByteString))).

#asOneByteString is a hack to eliminate any characters over an integer value of 255.  We have Mac users that are able to paste in characters that the XML.Document will not tolerate. 

Collections are just nested nodes...

buildCommentsXML: aNode
"allow for comments as strings, for initial conversion to comment objects"
| listNode | 
self comments notEmpty ifTrue: [
aNode addNode: (listNode := XML.Element tag: 'comments').
self comments do: [:each | 
listNode addNode: (each buildCommentsXML)]].

Where #buildCommentsXML adds text content to a new node...
  node := XML.Element tag: 'issuecomment'.

Simple, low tech and robust enough to handle our needs. 

1 comment:

  1. I see you looked at Gjallar! It has been dormant for quite some time but now and then I get the urge to refresh it and move it forward. It did end up a tad "over engineered" for both reasonable and unreasonable reasons :). Anyway, if you are interested in it - drop me an email.

    regards, Göran