HEAD PREVIOUS

Chapter 7
Issue Manager

In this chapter, we're going to discuss JMatter's support for business objects with lifecycles. We're going to write an Issue Manager, a familiar application for software developers who use such systems to help manage the development of software. Examples of issue managers include Bugzilla, Trac, and JIRA.

7.1  Analysis

The central type in this application is the Issue. The interesting aspect of issues is that they have a life cycle. An Issue is created and assigned to a developer. The developer accepts the issue and begins working on a fix. After resolving the issue, the person who opened the issue verifies that the resolution is indeed satisfactory, and proceeds to close the issue. If the issue is not resolved to satisfaction, the issue can be reopened.
It's also important to keep some kind of trail of activity: who opened the issue? Who resolved it and when? An end user should have the ability to attach notes to an issue, to specify a description of the issue, steps to reproduce the problem (if applicable).
The main trait of objects with life cycles is that their behaviour depends on their state. For example, one cannot attempt to accept an issue if it's already been closed. We can identify the various states for an Issue, and define its lifecycle with the aid of state diagrams, transition tables, etc.. Software developers will typically model such objects using the State Design Pattern22.
I have decided to model an Issue with five states:
  1. New
  2. Assigned
  3. Accepted
  4. Fixed
  5. Closed
In a normal flow, an issue moves sequentially through these states. An issue is created (New state), then assigned to a developer (Assigned state), and accepted by the developer (Accepted state). The person who opened the issue has the option of rejecting the fix (in which case its state reverts to Accepted) or approving it, moving Issue to the Closed state. An issue can also be reassigned to a different developer.

7.1.1  Modeling States in Java

In Java, the State pattern is often and conveniently modeled using inner classes. Each state is implemented as an inner class which controls the behaviour of the Context object (in this case, Issue) when in that state. Also, many texts encourage the design to use static inner classes for improving the performance of the system. That is, rather than create an instance of each inner class per Issue, a single instance of each inner class exists for all issues. The issue that is to be operated upon is passed in as an argument to that inner class's methods.
Although there's no denying the improvement in memory requirements, I personally find this design less than ideal, from an object-oriented point of view. I find it akin to removing the implicit this keyword in instances and passing a reference to self with each method.

7.2  Getting Started

We're about to start building our fourth application. Since we've done this together three times already, I'm not going to walk you through the steps of creating a new project and configuring it Please refer to previous chapters for specific instructions. I named my application IssueMgr.
Ok, we need to model Issue. As a first pass, let's not worry right away about the various states we identified in the previous section. Let's instead start be defining the various properties we want an issue to have. Here are the properties I've come up with:
private final StringEO _title = new StringEO();
private final TextEO _description = new TextEO();
   
// a loose definition (using a numeric value instead of an enumeration)
private final IntEO _priority = new IntEO();
private final IntEO _severity = new IntEO();
private Issue _dependsOn;
   
private final RelationalList _notes = new RelationalList(Note.class);
public static Class notesType = Note.class;
private User _openedBy;
private User _assignedTo;
private final RelationalList _history = 
                           new RelationalList(LoggedEvent.class);
public static Class historyType = LoggedEvent.class;
An issue then will have a title and a detailed description. Note how the description field is defined as a TextEO which causes it to be rendered using a text area, and saved as in the database as a large text type. I've decided for a first-pass to keep severity and priority really simple, as numeric values. The next one: dependsOn is interesting and fairly self-explanatory. It will give us the knowledge that Issue A will not be resolved before the issue it depends on (say Issue B) is resolved, for example.
The next field, notes, will allow us to tack on notes to an issue (as many notes as we want in fact). A Note is yet another predefined type in JMatter. It has a subject, a time stamp, an author, and the note text itself. When creating a note, the author is automatically assigned to the user who is currently logged in.
The next two fields, openedBy and assignedTo are self-explanatory. The last field is an interesting one. The idea is that I'd like to keep track of the history of an issue: when was it opened, assigned, accepted, fixed, etc.. Furthermore, when an issue is fixed, i want to require the developer to enter both a summary description of the fix and a lengthier one. I want to capture that information.
After thinking about this for a little while, I realized that JMatter's built-in LoggedEvent type would be a perfect candidate for recording this information. It's already designed to hold precisely this type of information. It has a timestamp, a message, long message, it records the user who performed the action, it can even record what action was performed, and finally, has an association back to the object upon which the operation was performed. In the context of the issue manager, we'll be able to navigate from a log entry (with message Issue fixed at 4:30 pm) back to the issue it is associated to.
Ok, it looks like we have a fairly complete list of fields to start with. Of course, it's not enough to just define these fields, the JMatter conventions must be followed: define accessor methods for aggregate types and both accessors and mutators for association types, following the JavaBeans bound property convention.
public StringEO getTitle() { return _title; }
public TextEO getDescription() { return _description; }
public IntEO getPriority() { return _priority; }
public IntEO getSeverity() { return _severity; }
public Issue getDependsOn() { return _dependsOn; }
public void setDependsOn(Issue issue)
{
   Issue oldValue = _dependsOn;
   _dependsOn = issue;
   firePropertyChange("dependsOn", oldValue, _dependsOn);
}
public RelationalList getNotes() { return _notes; }
public User getOpenedBy() { return _openedBy; }
public void setOpenedBy(User user)
{
   User oldValue = _openedBy;
   _openedBy = user;
   firePropertyChange("openedBy", oldValue, _openedBy);
}
public User getAssignedTo() { return _assignedTo; }
public void setAssignedTo(User user)
{
   User oldValue = _assignedTo;
   _assignedTo = user;
   firePropertyChange("assignedTo", oldValue, _assignedTo);
}
public RelationalList getHistory() { return _history; }
Here is the context within which all this code is written:
@Persist
public class Issue extends AbstractComplexEObject
{
   public static String[] fieldOrder = {"title", "description", "notes",
         "openedBy", "assignedTo", "history", "severity", "priority"};
   public Issue() {}
   ...
   public Title title()
   {
      return _title.title().appendParens(""+getID());
   }
}
We added the familiar fieldOrder metadata and the required title() method. Since it is customary to refer to issues by some unique numeric ID, I'm exposing the issue's ID property in its title. This property is inherited from AbstractComplexEObject.
We normally do not edit src/persistClasses.st by hand: the @Persist annotation takes care to add Issue to the list automatically. However here we're using a predefined entity Note and for it we do need to manually add an entry to our persistClasses.st file, like so:
..
<value>com.u2d.type.composite.Note</value>
..
We can now generate and export our schema, run the application and create a few issues.

7.3  Modeling Issues' Lifecycle

The time has come to model issues' lifecycle.
There are a number of concerns here. The first is keeping track of the state of our issue. And more specifically, to ensure that when an issue is persisted to the database, that its state is remembered and properly restored at a later point in time.
A simple way to do this is to define yet another field to hold the name of the state. This field will be persisted to the database just like Issue's other fields, and can be the basis for restoring Issues' state when reloading issues from the database.
private final IssueState _status = new IssueState(NEW);
Where NEW is one of a number of static string-based constants:
static final String NEW = "New";
static final String ASSIGNED = "Assigned";
static final String ACCEPTED = "Accepted";
static final String FIXED = "Fixed";
static final String CLOSED = "Closed";
We have identified five states and they're not likely to change. JMatter provides a mechanism for modeling enumerations by extending the JMatter type ChoiceEO. Here is the implementation of the contract to define the IssueState enumeration:
public class IssueState extends ChoiceEO
{
   public IssueState() {}
   public IssueState(String value) { setValue(value); }
   
   private static Set STATUS_OPTIONS = new HashSet();
   static
   {
      STATUS_OPTIONS.add(Issue.NEW);
      STATUS_OPTIONS.add(Issue.ASSIGNED);
      STATUS_OPTIONS.add(Issue.ACCEPTED);
      STATUS_OPTIONS.add(Issue.FIXED);
      STATUS_OPTIONS.add(Issue.CLOSED);
   }
   
   public Collection entries() { return STATUS_OPTIONS; }
}
Nothing too interesting really. We also must remember to add an accessor method for status:
public IssueState getStatus() { return _status; }

7.3.1  Defining the State Inner Classes

Below I've defined five inner classes, one for each of the states that Issue can be in.
public class NewState extends ReadState {}
public class AssignedState extends ReadState
{
   @Cmd(mnemonic='a')
   public void Accept(CommandInfo cmdInfo)
   {
      transition(_acceptedState, makeLog("Issue accepted by developer"));
   }
}
public class AcceptedState extends ReadState
{
   @Cmd
   public void Fix(CommandInfo cmdInfo,
                   @Arg("Fix") StringEO fix,
                   @Arg("Description") TextEO description)
   {
      transition(_fixedState, 
                 makeLog("Fix: "+fix.stringValue(), description));
   }
}
public class FixedState extends ReadState
{
   @Cmd
   public void RejectFix(CommandInfo cmdInfo,
                         @Arg("Explanation") TextEO explanation)
   {
      transition(_acceptedState, makeLog("Fix rejected", explanation));
   }
   @Cmd
   public void Close(CommandInfo cmdInfo,
                     @Arg("Explanation") TextEO explanation)
   {
      transition(_closedState, makeLog("Issue Closed", explanation));
   }
}
   public class ClosedState extends ReadState {}
Notice that the convention for defining commands on these states is the same as the mechanism used for defining commands in general. The difference is that JMatter will ensure that these commands are accessible in a valid state. The @Arg annotations' values are a means of specifying the captions used to prompt the end user for each of the method parameters.
Although we've defined the classes, we must also create an instance for each:
private transient final State _newState, _assignedState, 
                _acceptedState, _fixedState, _closedState;
{
   _newState = new NewState();
   _assignedState = new AssignedState();
   _acceptedState = new AcceptedState();
   _fixedState = new FixedState();
   _closedState = new ClosedState();
   _stateMap.put(_newState.getName(), _newState);
   _stateMap.put(_assignedState.getName(), _assignedState);
   _stateMap.put(_acceptedState.getName(), _acceptedState);
   _stateMap.put(_fixedState.getName(), _fixedState);
   _stateMap.put(_closedState.getName(), _closedState);
}
In addition to instantiation, I've also added each state to a map, defined in Issue's superclass. At the moment, this is a requirement of the framework. We haven't yet specified the starting state, so let's do that:
public State startState() { return _newState; }
We also need to provide a mechanism for the Issue's state to be restored after an issue is fetched from the database:
public State restoredState()
  return (State) _stateMap.get(getStatus().code());
}
Both of these are really simple. Here we see that we use the value of the issue's status (which was fetched from the database) as a means to fetch the state object from the state map.
I have not yet showed you the support code for the transitions. In each state where a command is defined a transition takes place. A logged event is created and passed in to the method named transition(). Here's how I create the logged event:
private LoggedEvent makeLog(String msg)
{
   LoggedEvent evt = (LoggedEvent) createInstance(LoggedEvent.class);
   evt.getMsg().setValue(msg);
   evt.getType().setValue(LoggedEvent.INFO);
   evt.setUser(currentUser());
   evt.setObject(this);
   return evt;
}
private LoggedEvent makeLog(String msg, TextEO longMsg)
{
   LoggedEvent evt = makeLog(msg);
   evt.getLongMsg().setValue(longMsg);
   return evt;
}
I provide two utility methods for constructing logged events. The first does not require a long message, the second is an overloaded version that also sets the long message. Notice how commands such as Fix, Close and RejectFix specify arguments. Recall that JMatter "automagically" prompts the end user for these arguments in the user interface and then feeds them to the commands when invoking their methods. These values are then passed in to the makeLog() methods.
Finally, here is the implementation of the transition() method:
private void transition(State state, LoggedEvent evt)
{
   _history.add(evt);
   setState(state, true);
   _status.setValue(state.getName());
   persistor().updateAssociation(this, evt);
}
We see here that the logged event is added to the history field, the state transition takes place, the status field is updated accordingly (kept in sync with the issue's state). The final statement ensures that the updated issue and its association to the newly created logged event are saved to the database.

7.4  Per-State Icons

Up until now, you've been told that for each type of object you define, that JMatter will look for an image file with a specific naming convention to use to represent objects of that type. This is true, but it's not the whole story.
Let's take an example. For the class Issue, we provide Issue32.png and Issue16.png. However, you're also free to provide additional icons, one for each state: IssueAssigned32.png, IssueAccepted32.png, etc.. So we see here an extension of the convention. For objects with lifecycles, the icon can be made to further reflect the state of the object you're viewing. I have specified custom icons for each of Issue's five states. Here's (figure 7.1) a screenshot of the icons in my resources/images directory, along with their corresponding file names.
figures/IssueMgr-1.png
Figure 7.1: State-Specific Icon Support

7.5  Additional Metadata

We've defined a number of commands, such as Fix(CommandInfo cmdInfo, StringEO fix, TextEO description). We customized the captions for the two arguments fix, and description so the end user is clear about what information he or she will have to enter. We did this by annotating the method's arguments with @Arg annotations, like this:
public void Fix(CommandInfo cmdInfo,
                      @Arg("Fix") StringEO fix,
                      @Arg("Description") TextEO description)
The annotation takes a single argument, the parameter caption.
There's a second, much more important issue that needs to be addressed though. In the case of our issue manager application, it's not enough that the command accept be only accessible in Assigned state. Only the developer who has been assigned the particular issue should be allowed to accept the issue. The same applies to the fix command. For rejectFix and close, the same idea applies: only the user who opened the issue should be allowed to close it, not the developer.
JMatter provides a means to specify what user is the owner of a command. A command's owner is the only user who will be allowed to invoke it. JMatter will not even display the command's views (a button or a menu item) to any other user. Here's how this is done:
static
{
   ComplexType type = ComplexType.forClass(Issue.class);
   type.command("Accept", AssignedState.class).setOwner(type.field("assignedTo"));
   type.command("Fix", AcceptedState.class).setOwner(type.field("assignedTo"));
   type.command("RejectFix", FixedState.class).setOwner(type.field("openedBy"));
   type.command("Close", FixedState.class).setOwner(type.field("openedBy"));
}
I hope you'll agree this is fairly terse, yet clear and legible code. The last line, for example, interprets to "the owner for the command named Close (in Fixed state) is the value of the issue's openedBy field. In other words, whoever opened the issue is the one authorized to close it.

7.6  A Few Loose Ends

7.6.1  Default Assigned-To Developer

It sure would be nice if each time I created a new issue, a certain developer would be the default user assigned to the issue. One way to do this is to specify the default in the file resources/model-metadata.properties, like this:
#
Issue.assignedTo.default=from User as user where user.username='eitan'
We've already used this file to specify field metadata such as whether and which fields are required. Here we're specifying a default. We can either hard-code it, or specify any valid hql (hibernate query language) that will return an instance of a valid type.

7.6.2  Automatically Setting OpenedBy

Each time we create an issue, the person who opened the issue is by definition the currently logged in user. How do we programmatically specify that this should automatically happen? Here is one way to do this:
public void onBeforeCreate()
{
   super.onBeforeCreate();
   setOpenedBy(currentUser());
}
This method overrides a superclass method, one that is notified prior to the creation of an object. It turns out that persistent objects (such as Issue) are not the only ones that can listen to various object persistence lifecycle events. JMatter provides a generic notification mechanism that any object can take advantage of. Each type of event is defined by a string constant, such as DELETE, SAVE, BEFORECREATE, CREATE. You'll see an example use of this mechanism shortly. It's also worth noting that besides object persistence events, JMatter provides hooks for application events such as login and logout events.

7.6.3  Transitioning to AssignedState

You might have noticed that I have left a glaring omission: how exactly does an issue transition to assigned state? The transition should take place when a user is associated to the assignedTo property of Issue.
We need to be careful here. The setter method is called not only when an association is made but also when the object is restored from the persistence store (the database). To distinguish between these two contexts, JMatter allows the definition of an additional method, the associate method. It works like this: if both a setter and an associator are defined, JMatter will make sure to call the associator only when associating (calling only the setter when restoring the property from db).
Here's the implementation:
public void associateAssignedTo(User user)
{
   setAssignedTo(user);
      
   if (_assignedTo != null && !_assignedTo.isEmpty())
   {
      if (isEditableState())
      {
         addAppEventListener(ONCREATE, new AppEventListener()
         {
            public void onEvent(AppEvent appEvent)
            {
               transition(_assignedState, 
                          makeLog("Assigned to "+_assignedTo));
            }
         });
      }
      else
      {
         transition(_assignedState, makeLog("Assigned to "+_assignedTo));
      }
   }
}
This code looks a little complicated. It has to concern itself with a specific issue. A call to the transition() method has the side effect of saving everything and putting the object in read state. We want to delay the call to transition if the association is made while the issue is in an editable state (before it has been saved, while it's being edited).
I'm resorting to using JMatter's application event notification mechanism. If the association is made in read state, I simply transition. Otherwise, I delay transitioning until after the editing is complete.

7.7  Issue Categories

Our implementation of Issue is now complete. Let's add one last feature. It might be helpful to define various categories of issues and classify issues according to these categories.
The first thing we do is define an issue category type:
@Persist
public class IssueCategory extends AbstractComplexEObject
{
   private final StringEO _name = new StringEO();
   public IssueCategory() {}
   public StringEO getName() { return _name; }
   public Title title() { return _name.title(); }
}
Nothing fancy here. An issue category is defined to have a single field: a name.
Rather than define a to-many relationship to issue, I'm going to add a command that will fetch the category's issues from the database and return a paged list:
@Cmd
public Object Issues(CommandInfo cmdInfo)
{
   ComplexType type = ComplexType.forClass(Issue.class);
   FieldPath path = new FieldPath("com.u2d.issuemgr.Issue#category");
   QuerySpecification spec = new QuerySpecification(path,
         new IdentityInequality().new Equals(), this);
   SimpleQuery query = new SimpleQuery(type, spec);
   return new PagedList(query);
}
I'm not entirely satisfied with the implementation. I'd like to be able to use plain old hql or the hibernate API to define this query. The above uses JMatter's own API for defining queries, which basically says: fetch all issues where the category equals "this".
On the issue side, we need to add a to-one association to the issue category:
private IssueCategory _category;
public IssueCategory getCategory() { return _category; }
public void setCategory(IssueCategory category)
{
   IssueCategory oldValue = _category;
   _category = category;
   firePropertyChange("category", oldValue, _category);
}
We also need to update our fieldOrder metafield:
public static String[] fieldOrder = {"status", "title", "description", 
      "notes", "openedBy", "assignedTo", "history", "severity", 
      "priority", "category"};
That's it for the coding. Make sure to update the schema and let's run the application.

7.8  The Application

Just because we finished coding does not mean that we're done configuring our application. For example, it might be useful to define a few standard queries such as List Outstanding Issues and List Closed Issues that pre-filters the listing (using JMatter's Smart List mechanism).
Figure 7.2 below shows the issue manager in action. I'm listing all issues to illustrate how the issue's icon reflects its state (the ones with a lock are closed issues, the ones with the pencil are accepted, and supposedly, being worked on). I'm also showing two smart lists that I've created and that I often use to check out outstanding issues.
figures/IssueMgr-2.png
Figure 7.2: The Issue Manager
I also often view issues in tabular view, which allows me to sort issues by priority or severity (sorting is invoked by clicking on the table column header).

7.9  Summary

As in previous chapters, allow me to show you the stats for this application, in terms of lines of code:
eitan@ubuntu:/projects/ds/IssueMgr/src/com/u2d/issuemgr$ wc -l *.java
  40 IssueCategory.java
 232 Issue.java
  28 IssueState.java
 300 total
Three classes totaling 300 lines of code. The current state of lifecycle support in JMatter is pretty strong. Its implementation is a natural extension of the conventions already established for simpler business objects. Nevertheless, I believe the implementation can be streamlined further and place even less requirements on the developer. For example the requirement to add the various states to a state map could be done by the framework.
In summary, support for business objects lifecycle is a necessary component of supporting the development of business applications in general. Many business objects naturally embody lifecycles, including orders (new, confirmed, fulfilled, etc..), visits (scheduled, canceled, confirmed, ongoing, archived), etc..
We have now covered four distinct applications: a contact manager, the MyTunes application, the Sympster conference manager, and finally our issue manager in a very short time. JMatter also comes with a movie library application, that, among other things, illustrates many-to-many relationships, as well as two other demonstration applications that illustrate how to integrate custom views into your application (the topic of chapter ).
Let's now turn our attention to part , which covers the JMatter framework in detail.

Part 3
Framework Reference



HEAD NEXT